This repository has been archived by the owner on Sep 30, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
/
vagrant-dynamic-inventory
executable file
·1023 lines (900 loc) · 39.7 KB
/
vagrant-dynamic-inventory
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env php
<?php
/*
* Vagrant Ansible dynamic inventory
*
* This script is designed to act as an Ansible dynamic inventory, each time it is run a instantaneous representation
* of the current Vagrant environment is returned.
*
* Maintainer: Felix Fennell <[email protected]>, Web & Applications Team <[email protected]>, British Antarctic Survey
* Source: https://github.com/felnne/ansible-dynamic-inventory-vagrant
*
* Note: This script is a proof-of-concept!
* Note: A Vagrant environment needs to be running before running this script (i.e. you've already run '$ vagrant up')
* Note: If you want to see what this inventory will look like you can run this script directly.
*
* Copyright: NERC-BAS 2016
* License: MIT license
* See the README.md file for more copyright and licensing information
*/
/*
* Start up - Start script timer
*/
$timeStart = microtime(true);
/*
* Initialisation - Set up key data-structures
*/
// Hold options for this script
$options = [];
// Hold information about hosts and how to connect to them - used to build host information in the dynamic inventory
$hosts = [];
// Hold information how hosts can be grouped together - used to build group information in the dynamic inventory
$groups = [];
// Hold the contents of the dynamic inventory
$inventory = [];
/*
* Initialisation - Set default script settings / options
*/
$defaultOptions = [
'working_directory' => '../..',
'fqdn_domain' => '.test',
'debug' => false
];
/*
* Initialisation - Load script settings / options from INI configuration file
*/
$options = array_merge($defaultOptions, parse_ini_file("vagrant-dynamic-inventory.ini"));
if ($options['debug']) {
echo "[DEBUG] Resolved settings/options";
var_dump($options);
}
/*
* Initialisation - Switch to working directory
*/
// Back up of the initial working directory this script was ran in - in case this is needed in the future
$startupWorkingDirectory = getcwd();
switch_to_working_directory($options['working_directory']);
/*
* Message input - use Vagrant to generate script input and split this into 'messages' of information
*/
$rawVagrantOutput = get_input_from_vagrant();
$processedVagrantOutput = process_vagrant_output($rawVagrantOutput);
/*
* Message validation - check each 'message' is valid and from these select only those we are interested in
* Note: The format of messages is controlled by Vagrant: https://www.vagrantup.com/docs/cli/machine-readable.html
*/
$validMessages = get_valid_messages($processedVagrantOutput, $options['debug']);
$specificMessages = get_host_specific_messages($validMessages, $options['debug']);
$interestingMessages = get_interesting_messages($specificMessages, $options['debug']);
/*
* Message processing - use the data in each message to build up information on hosts and groups for the inventory
*/
// Build up host information
$hosts = get_hosts_from_messages($interestingMessages);
$hosts = get_host_details_from_messages($hosts, $interestingMessages, $options['fqdn_domain']);
// Build up group information
$groups = make_groups_from_hosts($hosts);
/*
* Inventory construction - format the relevant information on hosts and groups into an Ansible compatible inventory
*/
$inventory = make_inventory($hosts, $groups, $inventoryName = 'Vagrant', $source = getcwd() . '/' . 'Vagrantfile');
/*
* Inventory output
*/
echo $inventory;
/*
* Shutdown - Stop script timer and display result
*/
$timeEnd = microtime(true);
$timeTaken = $timeEnd - $timeStart;
echo '# [NOTICE] Inventory generated in ' . round($timeTaken, 3) . ' seconds';
// End on a new line
echo "\n";
/*
* Script Functions
*/
/**
* Calls Vagrant to discover information about running machines (hosts)
*
* Calls the Vagrant 'ssh-config' command to get information on machines (hosts) defined, including their name/hostname,
* provider (e.g. VMware) and the private key needed for SSH.
*
* @see process_vagrant_output() For how output from this function can be prepared as input into this script
*
* @return string Contains the 'stdout' from Vagrant, which consists of a number of potential messages
*
* @example get_input_from_vagrant();
*/
function get_input_from_vagrant()
{
return shell_exec('vagrant ssh-config --machine-readable');
}
/**
* Takes stdout from Vagrant and converts into potential messages
*
* @see get_input_from_vagrant() For getting Stdout from a Vagrant command
* @see get_valid_messages() For validating which potential messages are suitable for further processing
*
* @param string $rawVagrantOutput Stdout from Vagrant
* @return array An array of potential messages
*
* @example process_vagrant_output($rawVagrantOutput);
*/
function process_vagrant_output($rawVagrantOutput)
{
// Split Vagrant output into lines
$arrayOfLines = explode("\n", $rawVagrantOutput);
// Convert each 'line' from a 'serialised' array of fields, to a multi-dimensional array to make processing easier
// Not all of these lines are valid messages
$arrayOfLinesWithFields = array_map(function ($line) {
return explode(',', $line);
}, $arrayOfLines);
return $arrayOfLinesWithFields;
}
/**
* For a set of Vagrant messages, returns only those considered valid
*
* Messages are considered valid if have 4 or more elements (timestamp, target, type and data) as per the format defined
* by Vagrant - the suitability of the elements within each message elements is checked elsewhere.
*
* @see https://www.vagrantup.com/docs/cli/machine-readable.html For information on the format of Vagrant 'messages'
* @see get_host_specific_messages() For determining which valid messages are suitable for further processing
*
* @param array $messages A set of potential Vagrant messages to be validated
* @param bool $debug If 'true', return information on invalid messages, otherwise they are discarded
* @return array A set of valid messages
*
* @example get_valid_messages($processedVagrantOutput);
*/
function get_valid_messages(array $messages, $debug = false)
{
$validMessages = [];
foreach ($messages as $index => $message) {
if (count($message) >= 4) {
$validMessages[] = $message;
} elseif ($debug) {
echo make_debug('Message: ' . $index . ' invalid - ', $item = $message);
}
}
return $validMessages;
}
/**
* For a set of Vagrant messages, returns only those which address a specific machine (host)
*
* A message is said to be non-specific when it does not refer to a specific machine (host), these include errors or a
* more general message.
*
* @see get_valid_messages() For determining the validity of messages
* @see get_interesting_messages() For determining which host specific messages are suitable for further processing
*
* @param array $messages A set of previously validated messages
* @param bool $debug If 'true', return information on non-specific messages, otherwise they are discarded
* @return array A set of messages which specify a specific host
*
* @example get_host_specific_messages($specificMessages);
*/
function get_host_specific_messages(array $messages, $debug = false)
{
$specificMessages = [];
foreach ($messages as $index => $message) {
// In the Vagrant machine readable format, $message[1] is possibly the host a message belongs to
if (! empty($message[1])) {
$specificMessages[] = $message;
} elseif ($debug) {
echo make_debug('Message: ' . $index . ' not specific to a host - ', $item = $message);
}
}
return $specificMessages;
}
/**
* For a set of Vagrant messages, returns those only useful for building an inventory
*
* A message is said to be useful or interesting in this context if it contains information about the provider used for
* a host (e.g. VMware) or if it contains SSH configuration information such as the identity file to connect to a host.
*
* Vagrant uses a 'type' field with a set of controlled values allowing the type of message to be checked easily. Some
* message types use different formats for the remaining 'data' field(s) of a message, which can naturally be inferred
* from the message type.
*
* The types of message this function considers interesting are defined by '$interestingMessageTypes'. For 'metadata'
* type messages an additional '$interestingMessageMetadataKeys' variable is used for additional filtering.
*
* @see get_host_specific_messages() For determining which messages are specific to a host
* @see get_hosts_from_messages() For gathering any hosts specified in a set of messages
* @see get_host_details_from_messages() For gathering details about hosts specified in a set of messages
*
* @param array $messages A set of messages, ideally known to be specific to a host
* @param bool $debug If 'true', return information on non-interesting messages, otherwise they are discarded
* @return array A set of messages which are interesting for building an inventory
*
* @example get_interesting_messages($specificMessages);
*/
function get_interesting_messages(array $messages, $debug = false)
{
$interestingMessages = [];
$interestingMessageTypes = [
'metadata',
'ssh-config'
];
$interestingMessageMetadataKeys = [
'provider'
];
foreach ($messages as $index => $message) {
// In the Vagrant machine readable format, $message[2] is the type of message
if (in_array($message[2], $interestingMessageTypes)) {
// In the Vagrant machine readable format, if the message type is 'metadata', $message[3] is a key
if ($message[2] == 'metadata' && in_array($message[3], $interestingMessageMetadataKeys)) {
$interestingMessages[] = $message;
} elseif ($message[2] == 'ssh-config') {
$interestingMessages[] = $message;
} elseif ($debug) {
echo make_debug('Message: ' . $index . ' non-interesting metadata key - ', $item = $message);
}
} elseif ($debug) {
echo make_debug('Message: ' . $index . ' non-interesting message type - ', $item = $message);
}
}
return $interestingMessages;
}
/**
* For a set of Vagrant messages, returns a list of any of uniquely specified hosts
*
* Most vagrant messages are specific to a specific machine (host), for a set of messages this function will return an
* array of unique hosts. The name of each host will be used as a named index in the returned array, the value of each
* index is an empty array, designed to be populated with other information about each host.
*
* @see get_interesting_messages() For gathering a list of messages relatent to building an inventory
* @see get_host_details_from_messages() For populating a set of hosts from this function with additional information
*
* @param array $messages A set of messages which specify a host
* @return array An array with named index for each unique host, an empty array is set as the value for each index
*
* @example get_hosts_from_messages($interestingMessages);
*/
function get_hosts_from_messages(array $messages)
{
$hosts = [];
foreach ($messages as $message) {
if (! array_key_exists($message[1], $hosts)) {
$hosts[$message[1]] = [];
}
}
return $hosts;
}
/**
* Updates a set of hosts with information form a set of Vagrant messages
*
* The array of hosts passed to this function must use a host names as indexes and empty arrays as values.
* Each message is passed through a number of functions which can decode a specific piece of information for a specific
* kind of message. If the message is the right kind for a function it will update the relevant array for the host the
* message relates to.
*
* Additional functions create new information about a host, based on information gathered from messages combined with
* arguments to this function.
*
* @see get_hosts_from_messages() For gathering a list of hosts from a list of messages, formatted for this function
* @see get_interesting_messages() For gathering a list of messages relatent to building an inventory
* @see get_host_provider() For determining the provider for a host from a relevant Vagrant message
* @see get_host_hostname() For determining the hostname for a host from a relevant Vagrant message
* @see get_host_identity_file() For determining the identity file to connect to a host from a relevant Vagrant message
* @see set_host_fqdn() For constructing a Fully Qualified Domain Name from existing host information and a domain name
* @see make_groups_from_hosts() For creating groups based on characteristics about hosts and how they are managed
* @see make_inventory() For outputting host information in the Ansible inventory format
*
* @param array $hosts
* @param array $messages
* @param string $fqdnDomain
* @return array
*
* @example get_host_details_from_messages($hosts, $interestingMessages, $fqdnDomain = 'example.com');
*/
function get_host_details_from_messages(array $hosts, array $messages, $fqdnDomain = null)
{
foreach ($messages as $message) {
// For each message select the host it refers to
$host = $hosts[$message[1]];
// Each message will only apply to one of these functions - this function does not know which messages belong
// to which functions deliberately so as to make changing or adding functions as simple as possible
$host = get_host_provider($host, $message);
$host = get_host_hostname($host, $message);
$host = get_host_identity_file($host, $message);
// These functions set additional information using pre-defined values, or values derived from existing values
$host = set_host_fqdn($host, $fqdnDomain);
// Persist the updated host
$hosts[$message[1]] = $host;
}
return $hosts;
}
/**
* Determines the provider for a host from a relevant Vagrant message
*
* The provider is the underlying platform or application that provides a host. Typically in the case of Vagrant, this
* is a hypervisor such as VMware or Virtualbox. Vagrant provides this information in a 'metadata' type message.
*
* Messages which don't refer to this specific piece of information will be ignored by this function.
*
* @see get_host_details_from_messages() For building up information about hosts using these sort of functions
*
* @param array $host The host the decoded provider refers to
* @param array $message A Vagrant message, ideally a metadata message of the relevant type
* @return array Where a provider is present in the message, an updated host, otherwise an unmodified host
*
* @example get_host_provider($host, $message);
*/
function get_host_provider(array $host, array $message)
{
if ($message[2] != 'metadata' && $message[3] != 'provider') {
return $host;
}
// Record actual value for possible future use
$host['provider_raw'] = $message[4];
// Normalise 'provider' value
if ($message[4] == 'vmware_fusion') {
$host['provider'] = 'vmware_desktop';
}
return $host;
}
/**
* Determines the hostname for a host from a relevant Vagrant message
*
* The hostname is typically the same as the machine name in a Vagrantfile, but can be different. For an inventory the
* hostname is the required name in order for Ansible to connect to the host using SSH. Vagrant provides this
* information in a 'ssh-config' type message.
*
* Messages which don't refer to this specific piece of information will be ignored by this function.
*
* @see get_host_details_from_messages() For building up information about hosts using these sort of functions
*
* @param array $host The host the decoded provider refers to
* @param array $message A Vagrant message, ideally a ssh-config message
* @return array Where a hostname is present in the message, an updated host, otherwise an unmodified host
*
* @example get_host_hostname($host, $message);
*/
function get_host_hostname(array $host, array $message)
{
if ($message[2] != 'ssh-config') {
return $host;
}
// For some reason Vagrant gives the SSH connection information as both a string with newlines, and a formatted
// string. We don't actually care which is used, but we do care that everything ends up being repeated.
// We therefore ignore the second set of information by removing it.
$messageValue = explode('FATAL', $message[3])[0];
// Get the name of the host (e.g. 'foo-dev-node1') which translates to the short hostname (e.g. unqualified)
$host['hostname'] = strip_string(get_string_between($messageValue, 'Host', 'HostName'));
return $host;
}
/**
* Determines the identity file for a host from a relevant Vagrant message
*
* The identity file is typically a randomly generated private key created by Vagrant when the host is first created.
* For an inventory the identity file is required in order for Ansible to connect to the host using SSH. Vagrant
* provides this information in a 'ssh-config' type message.
*
* Messages which don't refer to this specific piece of information will be ignored by this function.
*
* @see get_host_details_from_messages() For building up information about hosts using these sort of functions
*
* @param array $host The host the decoded provider refers to
* @param array $message A Vagrant message, ideally a ssh-config message
* @return array Where an identity file is present in the message, an updated host, otherwise an unmodified host
*
* @example get_host_identity_file($host, $message);
*/
function get_host_identity_file(array $host, array $message)
{
if ($message[2] != 'ssh-config') {
return $host;
}
// For some reason Vagrant gives the SSH connection information as both a string with newlines, and a formatted
// string. We don't actually care which is used, but we do care that everything ends up being repeated.
// We therefore ignore the second set of information by removing it.
$messageValue = explode('FATAL', $message[3])[0];
// Get the path to the private key needed to connect to the host, which Vagrant will generate automatically
$host['identity_file'] = strip_string(get_string_between($messageValue, 'IdentityFile', 'IdentitiesOnly'));
return $host;
}
/**
* Constructs a Fully Qualified Domain Name (FQDN) for a host using a hostname and given domain name
*
* A FQDN is essentially a hostname followed by a domain name (e.g. hostname.domain.tld), anything after the first
* full stop, '.', is considered the domain name, anything before is considered the hostname.
*
* The domain name used can be any valid domain name, however it is strongly recommended to use a domain name you
* control, or that is reserved for internal/test purposes as per RFC 2606.
*
* If the hostname is not known for a host, the host is returned unmodified.
*
* @see get_host_details_from_messages() For building up information about hosts using these sort of functions
* @see get_host_hostname() For specifically determining the hostname for a host from a relevant Vagrant message
* @see https://tools.ietf.org/html/rfc2606 For a list of domain names and TLDs reserved for testing
*
* @param array $host The host the FQDN will apply to and which ideally contains a 'hostname' property
* @param string $fqdnDomain The domain name or TLD for use in constructing the FQDN
* @return array Where a host has a hostname, an updated host, otherwise an unmodified host
*
* @example set_host_fqdn($host, $fqdnDomain = 'example.com');
*/
function set_host_fqdn(array $host, $fqdnDomain)
{
if (! array_key_exists('hostname', $host) && empty($host['hostname'])) {
return $host;
}
// Create Fully Qualified Domain Name using the hostname and a common domain name
$host['fqdn'] = $host['hostname'] . $fqdnDomain;
return $host;
}
/**
* Creates a series of groups for a set of hosts based on their characteristics and how they are managed
*
* Most of these groups are derived from information known about hosts (such as its provider). Other groups can be made
* using other criteria, for example grouping all hosts managed using Vagrant.
*
* Each group consists of a named index in an array, the value of each index is an array of host FQDNs.
* These groups are directly comparable to Ansible inventory groups and are used for this purpose.
*
* To build each group, hosts and other information, are passed to a number of functions, each responsible for creating
* a specific group or series of groups (provider groups for example).
*
* Note: The term 'group' is used to represent a grouping of hosts in an abstract way (but implemented using an array
* of named arrays), and in specifically in terms of Ansible inventory groups.
*
* @see get_hosts_from_messages() For gathering a list of hosts from a list of messages
* @see get_host_details_from_messages() For populating a set of hosts from with information from a list of messages
* @see make_manager_group() For creating groups based on the manager used for a host (e.g. Vagrant)
* @see make_providers_group() For creating groups based on the provider used for a host (e.g. VMware)
* @see make_wsr_1_element_groups() For creating groups based on hostname's formatted according to WSR-1
* @see make_inventory() For outputting group information in the Ansible inventory format
*
* @param array $hosts The set of hosts to be grouped
* @param string $nameForManagerGroup The name of the manager (e.g. vagrant) for a group based on how hosts are managed
* @return array The set of groups created
*
* @example make_groups_from_hosts($hosts);
*/
function make_groups_from_hosts(array $hosts, $nameForManagerGroup = 'vagrant')
{
$groups = [];
$groups = make_manager_group($hosts, $groups, $nameForManagerGroup);
$groups = make_providers_group($hosts, $groups);
$groups = make_wsr_1_element_groups($hosts, $groups);
return $groups;
}
/**
* Creates groups for a set of hosts based on how they are managed
*
* Each host uses a 'manager', which is usually Vagrant, this group will contain all hosts.
*
* This group is useful in environments where this inventory is one of many, and may therefore contain multiple
* managers, each with their own group containing hosts they manage.
*
* Note: Assumptions may be made as to the name of these management groups by other scripts of provisioning systems.
* It is therefore strongly advised to use any defaults offered for these groups.
*
* @see make_groups_from_hosts() For creating a series of groups for hosts using these sort of functions
*
* @param array $hosts The set of hosts to be grouped
* @param array $groups The set of groups this function will append any new groups to
* @param string $manager The name of the manager for the given hosts
* @return array If new groups were added, an updated set of groups, otherwise an unmodified set of groups
*
* @example make_manager_group($hosts, $groups, $manager = 'vagrant');
*/
function make_manager_group(array $hosts, array $groups, $manager)
{
if (! array_key_exists($manager, $groups)) {
$groups[$manager] = [];
}
foreach ($hosts as $host) {
$groups[$manager][] = $host['fqdn'];
}
return $groups;
}
/**
* Creates groups for a set of hosts based on their provider
*
* Each host uses a 'provider', the underlying platform or application that provides a host. Typically in the case of
* Vagrant, this is a hypervisor such as VMware or Virtualbox.
*
* These groups are useful in environments where this inventory is one of many, and may therefore contain multiple
* providers (not typically local providers, but a mixture of local and remote providers for example), each with their
* own group containing hosts they 'provide for'.
*
* @param array $hosts The set of hosts to be grouped
* @param array $groups The set of groups this function will append any new groups to
* @return array If new groups were added, an updated set of groups, otherwise an unmodified set of groups
*
* @example make_providers_group($hosts, $groups);
*/
function make_providers_group(array $hosts, array $groups)
{
foreach ($hosts as $host) {
if (array_key_exists('provider', $host)) {
if (! array_key_exists($host['provider'], $groups)) {
$groups[$host['provider']] = [];
}
$groups[$host['provider']][] = $host['fqdn'];
}
}
return $groups;
}
/**
* Creates groups for a set of hosts based on elements from WSR-1 formatted hostname's
*
* If a host uses a WSR-1 formatted hostname (such as 'foo-dev-web1'), groups can be made for the various elements
* that make up this hostname, such as Project ('foo'), Environment ('dev') and Purpose ('web').
*
* Where hosts, or a specific host, don't use such hostname's they are safely ignored by this function.
*
* These groups are useful in both environments with just this inventory, and as part of environments with multiple
* inventories, targeting different WSR-1 Environments for example (e.g. Development using Vagrant, Production using
* another manager/inventory).
*
* @param array $hosts The set of hosts to be grouped
* @param array $groups The set of groups this function will append any new groups to
* @return array If new groups were added, an updated set of groups, otherwise an unmodified set of groups
*
* @example make_wsr_1_element_groups($hosts, $groups);
*/
function make_wsr_1_element_groups(array $hosts, array $groups)
{
$wsr1Elements = [
'project',
'environment',
'instance',
'purpose'
];
foreach ($hosts as $hostName => $hostDetails) {
$hostnameWSRDecoded = decode_wsr_1_hostname($hostName);
// Skip over hosts without a WSR-1 hostname
if (! $hostnameWSRDecoded) {
continue;
}
foreach ($wsr1Elements as $element) {
// Skip over WSR-1 elements that are not defined (usually only applies to 'instance')
if (empty($hostnameWSRDecoded[$element])) {
continue;
}
if (! array_key_exists($hostnameWSRDecoded[$element], $groups)) {
$groups[$hostnameWSRDecoded[$element]] = [];
}
$groups[$hostnameWSRDecoded[$element]][] = $hostDetails['fqdn'];
}
}
return $groups;
}
/**
* Creates an Ansible formatted inventory from a set of hosts and groups with a given name
*
* This function expects hosts to be structured as an array of named indexes containing an array of properties about
* each host (e.g. its Full Qualified Domain Name).
*
* This function expects groups to be structured as an array of named groups containing an array of hosts to be members
* of each group.
*
* This function is generic and does not assume an inventory is being generated for any specific purpose (e.g. this is
* not a function specific to building a Vagrant inventory). Therefore a descriptive name is required to identify the
* generated inventory, and a source is required to describe where the input for the script came from.
*
* To build the inventory, hosts, groups and other information, are passed to a number of functions, each responsible
* for creating sections of the inventory (host definitions for example).
*
* @see get_host_details_from_messages() For building up information about hosts
* @see make_groups_from_hosts() For building up information about groups of hosts
* @see make_inventory_introduction() Outputs introductory comments for an inventory
* @see make_inventory_hosts() Outputs hosts definitions for a set of hosts
* @see make_inventory_groups() Outputs group definitions for a set of groups
*
* @param array $hosts A set of hosts, each containing properties about each host
* @param array $groups A set of groups
* @param string $inventoryName A descriptive name for this inventory, displayed in comments in the inventory
* @param string $source The input used for creating an inventory
* @return string A constructed inventory file which can be safely echoed
*
* @example make_inventory($hosts, $groups, $inventoryName = 'Vagrant');
*/
function make_inventory(array $hosts, array $groups, $inventoryName, $source)
{
$inventory = [];
// Start with an introduction
$inventory = array_merge($inventory, make_introduction($inventoryName, $source));
// Next add host information
// (fqdn & identity file)
$inventory = array_merge($inventory, make_inventory_hosts($hosts));
// Next add group information
$inventory = array_merge($inventory, make_inventory_groups($groups));
// End the inventory with an empty line
$inventory[] = "\n";
// Convert the inventory to a new-line joined string
$inventory = implode("\n", $inventory);
return $inventory;
}
/**
* Makes introductory lines for the inventory as comments
*
* This is mostly a formality, stating the output is dynamic and should not be modified, and stating which inventory
* it is and where the data came from. Since these dynamic inventories are designed to be consumed by Ansible directly
* only minimal information is outputted. It is mainly intended for users when running the dynamic inventory from the
* command line for debugging.
*
* @see make_inventory() For building up an inventory using these sort of functions
*
* @param string $inventoryName The descriptive name for the inventory
* @param string $source The input used for creating an inventory
* @return array A set of lines to be added to the inventory
*
* @example make_inventory_introduction($inventoryName = 'Vagrant');
*/
function make_introduction($inventoryName, $source)
{
$inventory = [];
$inventory[] = '# ' . $inventoryName . ' - Ansible dynamic inventory';
$inventory[] = '# The contents of this inventory are generated by a script, do not modify manually';
$inventory[] = '# Source: ' . $source;
return $inventory;
}
/**
* Makes host definition lines for the inventory for a set of hosts
*
* These definitions consist of the hostname and the identity file Ansible requires to connect to a host. The hostname
* is either the FQDN (if specified, preferred), the hostname (if specified) or the name of the host given by Vagrant.
*
* @see make_inventory() For building up an inventory using these sort of functions
*
* @param array $hosts A set of hosts to be defined within the inventory
* @return array A set of lines to be added to the inventory
*
* @example make_inventory_hosts($hosts);
*/
function make_inventory_hosts(array $hosts)
{
$inventory = [];
$inventory[] = "\n";
$inventory[] = '## Hosts';
foreach ($hosts as $hostName => $hostDetails) {
// Prefer a FQDN over a hostname and fall back to the machine name
if (array_key_exists('fqdn', $hostDetails)) {
$line = $hostDetails['fqdn'];
} elseif (array_key_exists('hostname', $hostDetails)) {
$line = $hostDetails['hostname'];
} else {
$line = $hostName;
}
// If an identity file is defined append to the host definition
if (array_key_exists('identity_file', $hostDetails) && $hostDetails['identity_file'] != '') {
$line .= " ansible_ssh_private_key_file='" . $hostDetails['identity_file'] . "'";
}
$inventory[] = $line;
}
return $inventory;
}
/**
* Makes group definition lines for the inventory for a set of groups
*
* These definitions consist of the name of the group, followed by the members of that group. Each group is separated
* with a new line character to improve readability.
*
* @see make_inventory() For building up an inventory using these sort of functions
*
* @param array $groups A set of groups to be defined within the inventory
* @return array A set of lines to be added to the inventory
*
* @example make_inventory_groups($groups);
*/
function make_inventory_groups($groups)
{
$inventory = [];
$inventory[] = "\n";
$inventory[] = '## Groups';
foreach ($groups as $groupName => $groupMembers) {
$inventory[] = '[' . $groupName . ']';
// Merge in members (hosts or group names) of group - empty or falsy values are omitted
$inventory = array_merge($inventory, array_filter($groupMembers));
// Add separator after each group
$inventory[] = '';
}
// Remove trailing separator
array_pop($inventory);
return $inventory;
}
/*
* Utility functions
*/
/**
* Sets the working directory for this script
*
* @param string $workingDirectory The working directory to switch to
* @return void
*
* @example switch_to_working_directory('/srv/project/');
*/
function switch_to_working_directory($workingDirectory)
{
chdir($workingDirectory);
}
/**
* Gets the substring between, and not including, a start and end string
*
* @param string $string The complete string, which contains the start/end strings and desired sub-string in between
* @param string $start The string that defines the start of the desired substring, but is not part of the substring
* @param string $end The string that defines the end of the desired substring, but is not part of the substring
* @return string The desired substring, or an empty string if the start string is not found in the complete string
*
* @example get_string_between('Foo Bar Baz', 'Foo', 'Baz'); // returns 'Bar'
*/
function get_string_between($string, $start, $end)
{
$string = ' ' . $string;
$ini = strpos($string, $start);
if ($ini == 0) {
return '';
}
$ini += strlen($start);
$len = strpos($string, $end, $ini) - $ini;
return substr($string, $ini, $len);
}
/**
* Removes leading/trailing spaces and optionally, single/double quotes and newlines from a string
*
* @param string $string The string to be 'stripped'
* @param bool $strip_quotes If 'true' single or double quotes will be removed, 'true' by default
* @param bool $strip_newlines If 'true' new line characters will be removed, 'true' by default
* @return string The 'stripped' string
*
* @example trim_string(' "foo" \n bar '); // returns 'foo bar'
*/
function strip_string($string, $strip_quotes = true, $strip_newlines = true)
{
if ($strip_quotes) {
// Strip single or double quotes
$string = str_replace('"', '', str_replace("'", "", $string));
}
if ($strip_newlines) {
// Strip \n and \r
$string = str_replace('\n', '', str_replace("'", "", $string));
$string = str_replace('\r', '', str_replace("'", "", $string));
}
// Strip leading/trailing spaces
return ltrim(rtrim($string));
}
/**
* Attempts to decode a WSR-1 formatted hostname return its separate elements
*
* A WSR-1 formatted hostname such as 'pristine-wonderment-of-the-ages-dev-felnne-db1' will be split into its elements:
* - Project 'pristine-wonderment-of-the-ages'
* - Environment 'dev'
* - Instance 'felnne'
* - node 'db1'
* - purpose 'db'
* - index '1'
*
* Non-optional elements that can't be found in a hostname will cause this function to fail and return an error.
* Optional elements that can't be found in a hostname will be returned as 'null' values.
* Hostname's which are not found to be WSR-1 compliant will cause this function to fail and return an error.
*
* @see decode_wsr_1_node_type() For how to decode the Node element into its sub-elements
*
* @param string $hostname The hostname to decode
* @return array|bool The elements of the hostname which could be decoded, or 'false' if an error occurred
*
* @example decode_wsr_1_hostname($hostname = 'pristine-wonderment-of-the-ages-dev-felnne-db1');
*/
function decode_wsr_1_hostname($hostname)
{
$project = null;
$environment = null;
$instance = null;
$node = null;
$purpose = null;
$index = null;
// The list of valid values for the environment of a WSR-1 hostname are controlled
$validEnvironments = [
'dev',
'stage',
'test',
'demo',
'prod'
];
// WSR-1 hostname's use '-' to separate elements
$elements = explode('-', $hostname);
// $project, $environment and $instance are required - so if there aren't at least this many elements this hostname
// is not WSR-1 compliant
if (count($elements) < 3) {
return false;
}
// The options for the $environment are a controlled list, and therefore predictable, we can then use the position
// of where the environment appears to workout the $project (which will come before the environment) and the
// $instance and $node which will appear afterwards
// First check a valid environment was used
if (empty(array_intersect($validEnvironments, $elements))) {
return false;
}
// Second find which environment is used and where is appears in the array
$environmentIndex = false;
foreach ($validEnvironments as $validEnvironment) {
$environmentIndexInstance = array_search($validEnvironment, $elements);
if ($environmentIndexInstance !== false) {
$environmentIndex = $environmentIndexInstance;
$environment = $elements[$environmentIndex];
}
}
// Determine the project name by taking all hostname elements before the environment element
$project = implode('-', array_chunk($elements, $environmentIndex)[0]);
// Determine the project instance by taking the elements between the environment and the node (the last element)
$instanceElements = array_chunk($elements, $environmentIndex)[1];
array_pop($instanceElements);
array_shift($instanceElements);
// If there are any elements left, these are instance elements and should be combined together, otherwise omit
if (count($instanceElements) > 0) {
$instance = implode('-', $instanceElements);
}
// Determine the node name by taking the last element
$node = $elements[count($elements) - 1];
// Further decode the node type
$nodeDecoded = decode_wsr_1_node_type($node);
if ($nodeDecoded !== false && is_array($nodeDecoded)) {
if (array_key_exists('purpose', $nodeDecoded) && ! empty($nodeDecoded['purpose'])) {
$purpose = $nodeDecoded['purpose'];
}
if (array_key_exists('index', $nodeDecoded) && ! empty($nodeDecoded['index'])) {
$index = $nodeDecoded['index'];
}
}
// We can now package up the WSR elements and return them as an array
$output = [
'project' => $project,
'environment' => $environment,
'instance' => $instance,
'node' => $node,
'purpose' => $purpose,
'index' => $index
];
return $output;
}
/**
* Attempts to decode a node element from a WSR-1 formatted hostname and return its separate sub-elements
*
* A WSR-1 formatted hostname contains a 'Node' element, this is made up of a Purpose and an Index sub-element.
* A node such as 'db1' will be split into its sub-elements:
* - Purpose 'db'
* - Index '1'
*
* @see decode_wsr_1_hostname() For how to decode a WSR-1 hostname into its elements for this function
*
* @param string $nodeName The Node to decode
* @return array|bool The sub-elements of the Node, or 'false' if an error occurred
*
* @example decode_wsr_1_node_type($node);
*/
function decode_wsr_1_node_type($nodeName)
{
if ($nodeName == null) {
return false;
}
// Split the node into the name/purpose part and index part
$nodeElements = preg_split('/(?=\d)/', $nodeName, 2);
// If there are more than two elements, something went wrong
if (count($nodeElements) > 2) {
return false;
}