-
Notifications
You must be signed in to change notification settings - Fork 0
/
Tutorial.txt
1456 lines (1214 loc) · 61.8 KB
/
Tutorial.txt
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
Actuator Tutorial
=================
Actuator allows you to use Python to declaratively describe system
infra, configuration, and execution requirements, and then provision
them in the cloud.
1. Intro
2. Overview (as close to a tl;dr that's still meaningful)
3. Infra Model
4. Namespace Model
5. Configuration Model
6. Execution Model
7. Infra models
8. A simple Openstack example
9. Multiple Resources
10. Resource Groups
11. Model References and Context Expressions
12. Namespace models
13. An example
14. Dynamic Namespaces
15. Var objects
16. Configuration models
17. Declaring tasks
18. Declaring dependencies
19. Dependency expressions
20. Auto-scaling tasks
21. Config classes as tasks
22. Reference selection expressions
23. Execution Models (yet to come)
24. Orchestration-- putting it all together
Intro
-----
Actuator seeks to provide an end-to-end set of tools for spinning up
systems in the cloud, from provisioning the infra, defining the roles
that define the system and the names that govern their operation,
configuring the infra for the software that is to be run, and then
executing that system's code on the configured infra.
It does this by providing facilities that allow a system to be described
as a collection of models in a declarative fashion directly in Python
code, in a manner similar to various declarative systems for ORMs
(Elixir being a prime example). Being in Python, these models:
- can be very flexible and dynamic in their composition
- can be integrated with other Python packages
- can be authored and browsed in existing IDEs
- can be debugged with standard tools
- can be inspected for auditing and other purposes
- and can be factored into multiple modules of reusable sets of
declarative components
And while each model provides capabilties on their own, they can be
inter-related to not only exchange information, but to allow instances
of a model to tailor the content of other models.
Actuator uses a Python class as the basis for defining a model, and the
class serves as a logical description of the item being modeled; for
instance a collection of infrastructure resources for a system. These
model classes can have both static and dynamic aspects, and can
themselves be easily created within a factory function to make the
classes' content highly variable.
Actuator models can be related to each other so that their structure and
data can inform and sometimes drive the content of other models.
Overview
--------
Actuator splits the modeling space into four parts:
Infra Model
The infra model, established with a subclass of InfraModel, defines all
the infrastructure resources of a system and their inter-relationships.
Infra models can have fixed resources that are always provisioned with
each instance of the model class, as well as variable-sized resource
containers that allow multiple copies of resources to be easily created
on an instance-by-instance basis. The infra model also has facilities to
define groups of resources that can be created as a whole, and an
arbitrary number of copies of these groups can be created for each
instance. References into the infra model can be held by other models,
and these references can be subsequently evaluated against an instance
of the infra model to extract data from that particular instance. For
example, a namespace model may need the IP address from a particular
server in an infra model, and so the namespace model may hold a
reference into the infra model for the IP address attribute that yields
the actual IP address of a provisioned server when an instance of that
infra model is provisioned.
Namespace Model
The namespace model, established with a subclass of NamespaceModel,
defines a hierarchical namespace based around system "roles" which
defines all the names that are important to the configuration and
run-time operation of a system. A "role" can be thought of as a software
component of a system; for instance, a system might have a database
role, an app server role, or a grid node role. Names in the namespace
can be used for a variety of purposes, such as setting up environment
variables, or establishing name-value pairs for processing template
files such as scripts or properties files. The names in the namespace
are associated with the namespace's roles, and each system role's view
of the namespace is composed of any names specific to that role, plus
the names that are defined higher up in the namespace hierarchy. Values
for the names can be baked into the model, supplied at model class
instantiation, by setting values on the model class instance, or can be
acquired by resolving references to other models such as the infra
model.
Configuration Model
The configuration model, established with a subclass of ConfigModel,
defines all the tasks to perform on the system roles' infrastructure
that make them ready to run the system's executables. The configuration
model defines tasks to be performed on the logical system roles of the
namespace model, which in turn inidicates what infrastructure is
involved in the configuration tasks. The configuration model also
captures task dependencies so that the configuration tasks are all
performed in the proper order. Configuration models can also be treated
as tasks and used within other configuration models.
Execution Model
The execution model, established with a subclass of ExecutionModel,
defines the actual processes to run for each system role named in the
namespace model. Like with the configuration model, dependencies between
the executables can be expressed so that a particular startup order can
be enforced.
Each model can be built and used independently, but it is the
inter-relationships between the models that give Actuator its
representational power.
Actuator then provides a number of support tools that can take instances
of these models and processes their informantion, turning it into
actions in the cloud. So for instance, a provisioner can take an infra
model instance and manage the process of provisioning the infra it
describes, and another can marry that instance with a namespace to fully
populate a namespace model instance so that the configurator can carry
out configuration tasks, and so on.
As may have been guessed, the key model in Actuator is the namespace
model, as it serves as the focal point to tie all the other models
together.
Infra models
------------
Although the namespace model is the one that is most central in
Actuator, it actually helps to start with the infra model as it not only
is a little more accessible, but building an infra model first can yield
immediate benefits. The infra model describes all the dynamically
provisionable infra resources and describes how they relate to each
other. The model can define groups of resources and resources that can
be repeated an arbitrary number of times, allowing them to be nested in
very complex configurations.
A simple Openstack example
The best place to start is to develop a model that can be used to
provision the infrastructure for a system. An infrastructure model is
defined by creating a class that describes the infra's resources in a
declarative fashion. This example will use resources built using the
Openstack binding to Actuator.
~~~~ {.python}
from actuator import InfraModel, ctxt
from actuator.provisioners.openstack.resources import (Server, Network, Subnet,
FloatingIP, Router,
RouterGateway, RouterInterface)
class SingleOpenstackServer(InfraModel):
server = Server("actuator1", "Ubuntu 13.10", "m1.small", nics=[ctxt.model.net])
net = Network("actuator_ex1_net")
fip = FloatingIP("actuator_ex1_float", ctxt.model.server,
ctxt.model.server.iface0.addr0, pool="external")
subnet = Subnet("actuator_ex1_subnet", ctxt.model.net, "192.168.23.0/24",
dns_nameservers=['8.8.8.8'])
router = Router("actuator_ex1_router")
gateway = RouterGateway("actuator_ex1_gateway", ctxt.model.router, "external")
rinter = RouterInterface("actuator_ex1_rinter", ctxt.model.router, ctxt.model.subnet)
~~~~
The order of the resources in the class isn't particularly important;
the provisioner will take care of sorting out what needs to be done
before what. Also note the use of 'ctxt.model.*' for some of the
arguments; these constructions are called context expressions as they
result in instances of the ContextExpr class, which are used to defer
the evaluation of a model reference until an instance of the model (the
"context") is available to evaluate the expression against.
Instances of the class (and hence the model) are then created, and the
instance is given to a provisioner which inspects the model instance and
performs the necessary provsioning actions in the proper order.
~~~~ {.python}
from actuator.provisioners.openstack.openstack import OpenstackProvisioner
inst = SingleOpenstackServer("actuator_ex1")
provisioner = OpenstackProvisioner(uid, pwd, uid, url)
provisioner.provision_infra_model(inst)
~~~~
Often, there's a lot of repeated boilerplate in an infra spec; in the
above example the act of setting up a network, subnet, router, gateway,
and router interface are all common resources needed to get access to
provisioned infra from outside the cloud. Actuator provides two ways to
factor out common groups of resources: providing a dictionary of
resources to the with_resources function, and using the ResourceGroup
wrapper class to define a group of standard resources. We'll recast the
above example using with_resources():
~~~~ {.python}
gateway_components = {"net":Network("actuator_ex1_net"),
"subnet":Subnet("actuator_ex1_subnet", ctxt.model.net,
"192.168.23.0/24", dns_nameservers=['8.8.8.8']),
"router":Router("actuator_ex1_router"),
"gateway":RouterGateway("actuator_ex1_gateway", ctxt.model.router,
"external"),
"rinter":RouterInterface("actuator_ex1_rinter", ctxt.model.router,
ctxt.model.subnet)}
class SingleOpenstackServer(InfraModel):
with_resources(**gateway_components)
server = Server("actuator1", "Ubuntu 13.10", "m1.small", nics=[ctxt.model.net])
fip = FloatingIP("actuator_ex1_float", ctxt.model.server,
ctxt.model.server.iface0.addr0, pool="external")
~~~~
With with_resources(), all the keys in the dictionary are established as
attributes on the infra model class, and can be accessed just as if they
were declared directly in the class. Since this is just standard keyword
argument notation, you could also use a list of "name=value" expressions
for the same effect.
Multiple resources
If you require a set of identical resources to be created in a model,
the MultiResource wrapper provides a way to declare a resource as a
template and then to get as many copies of that template created as
required:
~~~~ {.python}
from actuator import InfraModel, MultiResource, ctxt, with_resources
from actuator.provisioners.openstack.resources import (Server, Network, Subnet,
FloatingIP, Router,
RouterGateway, RouterInterface)
class MultipleServers(InfraModel):
#
#First, declare the common networking components with with_infra_components
#
with_resources(**gateway_components)
#
#now declare the "foreman"; this will be the only server the outside world can
#reach, and it will pass off work requests to the workers. It will need a
#floating ip for the outside world to see it
#
foreman = Server("foreman", "Ubuntu 13.10", "m1.small", nics=[ctxt.model.net])
fip = FloatingIP("actuator_ex2_float", ctxt.model.server,
ctxt.model.server.iface0.addr0, pool="external")
#
#finally, declare the workers MultiResource
#
workers = MultiResource(Server("worker", "Ubuntu 13.10", "m1.small",
nics=[ctxt.model.net]))
~~~~
The workers MultiResource works like a dictionary in that it can be
accessed with a key. For every new key that is used with workers, a new
instance of the template resource is created:
~~~~ {.python}
>>> inst2 = MultipleServers("two")
>>> len(inst2.workers)
0
>>> for i in range(5):
... _ = inst2.workers[i]
...
>>> len(inst2.workers)
5
>>>
~~~~
Keys are always coerced to strings, and for each new instance of the
MultiResource template that is created, the original name is appened
with '_{key}' to make each instance distinct.
~~~~ {.python}
>>> for w in inst2.workers.instances().values():
... print w.name
...
worker_1
worker_0
worker_3
worker_2
worker_4
>>>
~~~~
Resource Groups
If you require a group of different resources to be provisioned as a
unit, the ResourceGroup() wrapper provides a way to define a template of
multiple resources that will be provisioned as a whole. The following
example shows how the boilerplate gateway resources could be expressed
using a ResourceGroup().
~~~~ {.python}
gateway_component = ResourceGroup("gateway", net=Network("actuator_ex1_net"),
subnet=Subnet("actuator_ex1_subnet", ctxt.comp.container.net,
"192.168.23.0/24", dns_nameservers=['8.8.8.8']),
router=Router("actuator_ex1_router"),
gateway=RouterGateway("actuator_ex1_gateway", ctxt.comp.container.router,
"external"),
rinter=RouterInterface("actuator_ex1_rinter", ctxt.comp.container.router,
ctxt.comp.container.subnet))
class SingleOpenstackServer(InfraModel):
gateway = gateway_component
server = Server("actuator1", "Ubuntu 13.10", "m1.small", nics=[ctxt.model.gateway.net])
fip = FloatingIP("actuator_ex1_float", ctxt.model.server,
ctxt.model.server.iface0.addr0, pool="external")
~~~~
The keyword args used in creating the ResourceGroup become the
attributes of the instances of the group.
If you require a group of different resources to be provisioned together
repeatedly, the MultiResourceGroup() wrapper provides a way to define a
template of multiple resources that will be provioned together.
MultiResourceGroup() is simply a shorthand for wrapping a ResourceGroup
in a MultiResource. Any resource (including ResourceGroups and
MultiResources) can appear in a MultiResourceGroup.
~~~~ {.python}
from actuator import InfraModel, MultiResource, MultiResourceGroup, ctxt
from actuator.provisioners.openstack.resources import (Server, Network, Subnet,
FloatingIP, Router,
RouterGateway, RouterInterface)
class MultipleGroups(InfraModel):
#
#First, declare the common networking resources
#
with_resources(**gateway_components)
#
#now declare the "foreman"; this will be the only server the outside world can
#reach, and it will pass off work requests to the leaders of clusters. It will need a
#floating ip for the outside world to see it
#
foreman = Server("foreman", "Ubuntu 13.10", "m1.small", nics=[ctxt.model.net])
fip = FloatingIP("actuator_ex3_float", ctxt.model.foreman,
ctxt.model.foreman.iface0.addr0, pool="external")
#
#finally, declare a "cluster"; a leader that coordinates the workers in the
#cluster, which operate under the leader's direction
#
cluster = MultiResourceGroup("cluster",
leader=Server("leader", "Ubuntu 13.10", "m1.small",
nics=[ctxt.model.net]),
workers=MultiResource(Server("cluster_node",
"Ubuntu 13.10",
"m1.small",
nics=[ctxt.model.net])))
~~~~
The keyword args used in creating the ResourceGroup become the
attributes of the instances of the group; hence the following
expressions are fine:
~~~~ {.python}
>>> inst3 = MultipleGroups("three")
>>> len(inst3.cluster)
0
>>> for region in ("london", "ny", "tokyo"):
... _ = inst3.cluster[region]
...
>>> len(inst3.cluster)
3
>>> inst3.cluster["ny"].leader.iface0.addr0
<actuator.modeling.ModelInstanceReference object at 0x7fc9df79f090>
>>> inst3.cluster["ny"].workers[0]
<actuator.modeling.ModelInstanceReference object at 0x7fc9df79f250>
>>> inst3.cluster["ny"].workers[0].iface0.addr0
<actuator.modeling.ModelInstanceReference object at 0x7fc9df79f290>
>>> len(inst3.cluster["ny"].workers)
1
>>>
~~~~
This model will behave similarly to the MultiServer model above; that
is, the cluster attribute can be treated like a dictionary and keys will
cause a new instance of the MultiResourceGroup to be created. Note also
that you can nest MultiResources in MultiResourceGroups, and vice versa.
Model References and Context Expressions
A few of the examples above have shown that accessing model attributes
results in a reference object of some sort. These objects are the key to
declaratively relating aspects of various models to one another. For
instance, a reference to the attribute that stores the IP address of a
provisioned server can be used as the value of a variable in the
namespace model, and once the IP address is known, the variable will
have a meaningful value.
There are two different ways to get references to parts of a model:
first through the use of model references, which are direct attribute
accesses to model or model instance objects. This approach can only be
used after a model class has already been created; this means that if a
reference between memebers is required in the middle of a model class
definition, model references aren't yet available, and hence can't be
used.
The second method is through the use of context expressions. A context
expression provides a way to express a reference to objects and models
that don't exist yet-- the expression's evaluation is delayed until the
reference it represents exists, and only then does the expression yield
an actual reference. Additionally, context expressions provide a way to
express references that include keyed lookups into Multi* wrappers, but
will defer the lookup until needed. These two attributes allow context
expressions to be used in a number of ways that a direct model or
instance reference can't.
Model References
Once a model class has been defined, you can create expressions that
refer to attributes of resources in the class:
~~~~ {.python}
>>> SingleOpenstackServer.server
<actuator.modeling.ModelReference object at 0x7fc9df779d10>
>>> SingleOpenstackServer.server.iface0
<actuator.modeling.ModelReference object at 0x7fc9df779cd0>
>>> SingleOpenstackServer.server.iface0.addr0
<actuator.modeling.ModelReference object at 0x7fc9df779a10>
>>>
~~~~
Likewise, you can create references to attributes on instances of the
model class:
~~~~ {.python}
>>> inst.server
<actuator.modeling.ModelInstanceReference object at 0x7fc9df7280d0>
>>> inst.server.iface0
<actuator.modeling.ModelInstanceReference object at 0x7fc9df728110>
>>> inst.server.iface0.addr0
<actuator.modeling.ModelInstanceReference object at 0x7fc9df728150>
>>>
~~~~
All of these expressions result in a reference object, either a model
reference or a model instance reference. References are objects that
serve as a logical "pointer" to a resource or attribute of a model.
Model references are logical references into a model; there may not be
an actual resource or attribute underlying the reference. Model instance
references (or "instance references") are references into an instance of
a model; they refer to an actual resource or attribute (although the
value of either may not have been set yet). Instance references can only
be created relative to an instance of a model, or by transforming a
model reference to an instance reference using an instance of a model.
An example here will help:
~~~~ {.python}
#re-using the definition of SingleOpenstackServer from above...
>>> inst = SingleOpenstackServer("refs")
>>> modref = SingleOpenstackServer.server
>>> instref = inst.server
>>> modref is not instref
True
>>> instref is inst.get_inst_ref(modref)
True
>>>
~~~~
Model references provide a number of capabilities:
- They serve as bookmarks into models
- They behave something like a future in that they provide a reference
to a value that hasn't been determined yet
- They provide a way to make logical connections between models in
order to share information
- They serve as a way to logically identify resources that should be
provisioned
For example, suppose a model elsewhere needs to know the first IP
address on the first interface of the server from the
SingleOpenstackServer model. That IP address won't be known until the
server is provisioned, but a reference to this piece of information can
be created by the following expression:
~~~~ {.python}
SingleOpenstackServer.server.iface0.addr0
~~~~
The rest of Actuator knows how to deal with these references and how to
extract the underlying values when they become available. Every
attribute of all objects in a model produce a reference, and the
underying value that the reference is pointing to can be accessed with
the value() method:
~~~~ {.python}
>>> SingleOpenstackServer.server.name.value()
actuator1
>>>
~~~~
Since model references are the means to make connections between models,
we'll look at these in more detail in the section below on namespace
models.
Context Expressions
There are circumstances where model references either aren't possible or
can't get the job done. For example, take this fragment of the the
SingleOpenstackServer infra model example from above:
~~~~ {.python}
class SingleOpenstackServer(InfraModel):
router = Router("actuator_ex1_router")
gateway = RouterGateway("actuator_ex1_gateway", ctxt.model.router, "external")
rinter = RouterInterface("actuator_ex1_rinter", ctxt.model.router, ctxt.model.subnet)
#etc...
~~~~
The RouterGateway and RouterInterface resources both require a model
reference to a Router resource as their second argument. Now, after the
SingleOpenstackServer class is defined, this reference would be easy to
obtain with an expression such as SingleOpenstackServer.router. However,
within the class defintion, the class object doesn't exist yet, and so
trying to use an expression like:
~~~~ {.python}
gateway = RouterGateway("actuator_ex1_gateway", SingleOpenstackServer.router, "external")
~~~~
will yield a NameError exception saying that "SingleOpenstackServer" is
not defined.
This is where context expressions come in. Every time a component in a
model is processed by Actuator (be it a resource or some other
component), a processing context is created. The context wraps up:
- the instance of the model the component is part of,
- the component itself,
- and the name of the component.
In a model class, the context is referred to by the global object ctxt,
and the above three objects can be accessed via ctxt in the following
way:
- the model instance can be accessed via ctxt.model
- the component itself can be accessed via ctxt.comp
- the component's name can be accessed via ctxt.name
These context expressions provide a way to define a reference to another
part of the model that will be evaluated only when the reference is
needed. Repeating the infra model fragment from above:
~~~~ {.python}
class SingleOpenstackServer(InfraModel):
net = Network("actuator_ex1_net")
subnet = Subnet("actuator_ex1_subnet", ctxt.model.net, "192.168.23.0/24",
dns_nameservers=['8.8.8.8'])
router = Router("actuator_ex1_router")
gateway = RouterGateway("actuator_ex1_gateway", ctxt.model.router, "external")
rinter = RouterInterface("actuator_ex1_rinter", ctxt.model.router, ctxt.model.subnet)
#etc...
~~~~
We can see that we can provide the required reference to
SingleOpenstackServer's Router by creating a context expression that
names the router attribute of the SingleOpenstackServer model via the
ctxt object.
The context object ctxt allows you to access any attribute of a model or
component reachable from either the model or component. Hence, in the
same way we were able to access first IP address on the first interface
with:
~~~~ {.python}
SingleOpenstackServer.server.iface.addr0
~~~~
We can use a context expression to create a reference to this IP using
the ctxt object:
~~~~ {.python}
ctxt.model.server.iface.addr0
~~~~
As mentioned previously, context expressions provide a way to express
relationships between model components before the model is fully
defined. Additionally, because they allow references to be evaluated
later in processing, they are useful in certain circumstances in
creating references between models. We'll see examples of these sorts of
uses below.
Namespace models
----------------
The namespace model provides the means for joining the other Actuator
models together. It does this by declaring the logical roles of a
system, relating these roles to the infrastructure elements where the
roles are to execute, and providing the means to identify what
configuration task is to be carried out for each role as well as what
executables are involved with making the role function.
A namespace model has four aspects. It provides the means to:
1. ...define the logical execution roles of a system
2. ...define the relationships between logical roles and hosts in the
infra model where the roles are to execute
3. ...arrange the roles in a meaningful hierarchy
4. ...establish names within the hierachy whose values will impact
configuration activities and the operation of the roles
An example
Here's a trivial example that demonstrates the basic features of a
namespace. It will model two roles, an app server and a computation
engine, and use the SingleOpenstackServer infra model from above for
certain values:
~~~~ {.python}
from actuator import Var, NamespaceModel, Role, with_variables
class SOSNamespace(NamespaceModel):
with_variables(Var("COMP_SERVER_HOST", SingleOpenstackServer.server.iface0.addr0),
Var("COMP_SERVER_PORT", '8081'),
Var("EXTERNAL_APP_SERVER_IP", SingleOpenstackServer.fip.ip),
Var("APP_SERVER_PORT", '8080'))
app_server = (Role("app_server", host_ref=SingleOpenstackServer.server)
.add_variable(Var("APP_SERVER_HOST", SingleOpenstackServer.server.iface0.addr0)))
compute_server = Role("compute_server", host_ref=SingleOpenstackServer.server)
~~~~
First, some global Vars (variables) are established that capture the
host and port where the compute_server will be found, the external IP
where the app_server will be found, and the port number where it can be
contacted. While the ports are hard coded values, the host IPs are
determined from the SingleOpenstackServer model by creating a model
reference to the model attribute where the IP will become available.
Since these Vars are defined at the model (global) level, they are
visible to all roles.
Next comes the app_server role, which is declared with a call to Role.
Besides a name, Role is supplied a host_ref in the form of Server model
reference from the SingleOpenstackserver model. This tells the namespace
that this role's configuration tasks and executables will be run on
whatever host is provisioned for this part of the model. The app_server
role is also supplied a private Var object that captures the host IP
where the server will run. While the app_server binds to an IP on the
subnet, the FloatingIP associated with this subnet IP will enable the
server to be reached from the outside world.
Finally, we declare the compute_server Role. Similar to the app_server
Role, the compute_server Role identifies the Server where it will run by
setting the host_ref keyword to a infra model reference for the Server
to use. In this example, both Roles will be run on the same server.
When an instance of the namespace is created, useful questions can be
posed to the instance:
- We can ask for a list of roles
- We can ask for all the Vars (and their values) from the perspective
of a specific role
- We can identify any Vars whose value can't be resolved from the
perspective of each role
- We can ask to compute the necessary provisioning based on the
namespace and an infra model instance
These operations look something like this:
~~~~ {.python}
>>> ns = SOSNamespace()
>>> for r in ns.get_roles().values():
... print "Role: %s, Vars:" % r.name
... for v in r.get_visible_vars().values():
... value = v.get_value(r)
...
... print "%s=%s" % (v.name, value if value is not None else "<UNRESOLVED>")
...
Role: compute_server, Vars:
COMP_SERVER_HOST=<UNRESOLVED>
COMP_SERVER_PORT=8081
APP_SERVER_PORT=8080
EXTERNAL_APP_SERVER_IP=<UNRESOLVED>
Role: app_server, Vars:
APP_SERVER_HOST=<UNRESOLVED>
COMP_SERVER_HOST=<UNRESOLVED>
COMP_SERVER_PORT=8081
APP_SERVER_PORT=8080
EXTERNAL_APP_SERVER_IP=<UNRESOLVED>
>>> sos = SingleOpenstackServer("sos")
>>> provisionables = ns.compute_provisioning_for_environ(sos)
>>> provisionables
set([<actuator.provisioners.openstack.resources.RouterGateway object at 0x7fc9df72e610>, <actuator.provisioners.openstack.resources.Server object at 0x7fc9df72e450>, <actuator.provisioners.openstack.resources.FloatingIP object at 0x7fc9df72e090>, <actuator.provisioners.openstack.resources.Router object at 0x7fc9df72e6d0>, <actuator.provisioners.openstack.resources.RouterInterface object at 0x7fc9df72e510>, <actuator.provisioners.openstack.resources.Subnet object at 0x7fc9df72e490>, <actuator.provisioners.openstack.resources.Network object at 0x7fc9df72e590>])
>>>
~~~~
Dynamic Namespaces
The namespace shown above is static in nature. Although some of the
values for Var objects are supplied dynamically, the namespace itself
has a static number of roles and structure.
Actuator allows for more dynamic namespaces to be constructed, in
particular in support of arbitrary numbers of roles. By coupling such a
namespace with an infra model that uses MultiResource or
MultiResourceGroup elements, appropriately sized infra can be identified
and provisioned depending on the nature of the dynamic namespace.
The best way to understand this is with an example. We'll devise a
trivial computational grid: besides the normal gateway elements, the
infrastructure will contain a "foreman" to coordinate the computational
activities of a variable number of "workers", each on a seperate server.
The MultipleServers infra model from above fits this pattern, so we'll
define a dynamic namespace model that grows roles that refer back to
this infra model in order to acquire the appropriate infrastructure to
meet the namespace's needs.
We'll use two different techniques for creating a suitable dynamic
namespace. In the first, we'll create a class factory function that
defines a new namespace class with the appropriate number of worker
Roles. In the second, we'll use some additional features of Actuator to
express the same capabilities in a more concise declarative way.
First, the class factory approach:
~~~~ {.python}
def grid_namespace_factory(num_workers=10):
class GridNamespace(NamespaceModel):
with_variables(Var("FOREMAN_EXTERNAL_IP", MultipleServers.fip.ip),
Var("FOREMAN_INTERNAL_IP", MultipleServers.foreman.iface0.addr0),
Var("FOREMAN_EXTERNAL_PORT", "3000"),
Var("FOREMAN_WORKER_PORT", "3001"))
foreman = Role("foreman", host_ref=MultipleServers.foreman)
role_dict = {}
namer = lambda x: "worker_{}".format(x)
for i in range(num_workers):
role_dict[namer(i)] = Component(namer(i), host_ref=MultipleServers.workers[i])
with_roles(**role_dict)
del role_dict, namer
return GridNamespace()
~~~~
Making a dynamic namespace class in Python is trivial; by simply putting
the class statement inside a function, each call to the function will
generate a new class. By supplying parameters to the function, the
content of the class can be altered.
In this example, after setting some global Vars in the namespace with
the with_variables() function, we next create the "foreman" role, and
use the host_ref keyword argument to associate it with a server in the
infra model. Next, we set up a dictionary whose keys will eventually
become other attributes on the namespace class, and whose values will
become the associated Roles for those attributes. In a for loop, we then
simply create new instances of Role, associating each with a different
worker in the MultipleServers infra model
(host_ref=MultipleServers.workers[i]). We then use the function
with_roles() to take the content of the dict and attach all the created
roles to the namespace class. The class finishes by deleting the
unneeded dict and lambda function. The factory function completes by
returning an instance of the class that was just defined.
Now we can use the factory function to create grids of different sizes
simply by varying the input value to the factory function:
~~~~ {.python}
>>> ns = grid_namespace_factory(20)
>>> ms_inst = MultipleServers("ms")
>>> provs = ns.compute_provisioning_for_environ(ms_inst)
>>> len(provs)
27
>>> ns.worker_8
<actuator.namespace.Role object at 0x02670D10>
>>>
>>> ns2 = grid_namespace_factory(200)
>>> ms_inst2 = MultipleServers("ms2")
>>> provs2 = ns2.compute_provisioning_for_environ(ms_inst2)
>>> len(provs2)
207
>>>
~~~~
Now for the second approach, which utilizes some other capabilities of
Actuator. Namespaces have their own analogs to the infra model's
ResourceGroup, MultiResource, and MultiResourceGroup classes: they are
RoleGroup, MultiRole, and MultiRoleGroup. These are similar to their
infrastructure counterparts with the exceptions that
1. They can contain only Roles or the various Role containers mentinoed
above;
2. They can have variables (Var objects) attached to them.
Using this approach, the solution looks like the following:
~~~~ {.python}
class GridNamespace(NamespaceModel):
with_variables(Var("FOREMAN_EXTERNAL_IP", MultipleServers.fip.ip),
Var("FOREMAN_INTERNAL_IP", MultipleServers.foreman.iface0.addr0),
Var("FOREMAN_EXTERNAL_PORT", "3000"),
Var("FOREMAN_WORKER_PORT", "3001"))
foreman = Role("foreman", host_ref=MultipleServers.foreman)
grid = MultiRole(Role("node", host_ref=ctxt.model.infra.workers[ctxt.name]))
~~~~
This approach doesn't use a factory; instead, it uses MultiRole to
define a "template" Role object to create instances from each new key
supplied to the "grid" attribute of the namespace model. After defining
a namespace class this way, one simply creates instances of the class
and then, in a manner similar to creating new resources on an infra
model, uses new keys to create new Roles on the namespace instance.
These new role instances will in turn create new worker instances on a
MultiServers model instance:
~~~~ {.python}
>>> ns = GridNamespace()
>>> ms_infra = MultipleServers("ms1")
>>> for i in range(20):
... _ = ns.grid[i]
...
>>> provs = ns.compute_provisioning_for_environ(ms_infra)
>>> len(provs)
27
>>> ns2 = GridNamespace()
>>> ms_infra2 = MultipleServers("ms2")
>>> for i in range(200):
... _ = ns2.grid[i]
...
>>> provs2 = ns2.compute_provisioning_for_environ(ms_infra2)
>>> len(provs2)
207
~~~~
Using this approach, we can treat the namespace model like the infra
model, meaning that we can provide a logical definition of roles and
drive the creation of physical roles simply by referencing them. These
references flow through to the infra model, likewise causing dynamic
infra resources to be created.
Var objects
Namespaces and their Roles serve as containers for Var objects. These
objects provide a means to establish names that can be used symbolically
for a variety of purposes, such as environment variables for tasks and
executables, or parameter maps for processing templatized text files
such as scripts or properties files.
Vars associate a 'name' (the first parameter) with a value (the second
parameter). The value parameter of a Var can be one of several kinds of
objects: it may be a plain string, a string with a replacement paremeter
in it, a reference to an infra model element that results in a string,
or context expression that results in a string.
We've seen examples of both plain strings and model references as
values, and now will look at how replacement parameters and context
expressions work. A replacement parameter takes the form of !{string};
whenever this pattern is found, the inner string is extracted and looked
up as the name for another Var. The lookup repeats; if the value found
contains '!{string}', the lookup is repeated until no more replacement
parameters are found. This allows complex replacement patterns to be
defined.
Additionally, the hierarchy of roles, containers (MultiRole, RoleGroup,
and MultiRoleGroup) and the model class is taken into account when
searching for a variable. If the variable can't be found defined on the
current role, the enclosing variable container is searched,
progressively moving to the model class itself. If the variable can't be
found on the model class, then the variable is undefined, and an
exception may be raised (depending on how the search was initiated).
This allows for complex replacement patterns to be defined which have
different parts of the pattern filled in at different levels of the
namespace.
The following example will make this more concrete. Here we will create
a Namespace model that defines a variable "NODE_NAME" that is composed
of a base name plus an id specific to the node. While NODE_NAME will be
defined at a global level in the model, the two other variables the
comprise NODE_NAME, BASE_NAME and NODE_ID, will be defined on different
model objects.
~~~~ {.python}
>>> class VarExample(NamespaceModel):
... with_variables(Var("NODE_NAME", "!{BASE_NAME}-!{NODE_ID}"))
... grid = (MultiRole(Role("worker", variables=[Var("NODE_ID", ctxt.name)]))
... .add_variable(Var("BASE_NAME", "Grid")))
>>> ns = VarExample()
>>> ns.grid[5].var_value("NODE_NAME")
Grid-5
>>>
~~~~
At the most global level, the NODE_NAME Var is defined with a value that
contains two replacement parameter patterns. The first, BASE_NAME, is a
Var defined on the grid MultiRole object, and has a value of 'Grid'. The
second, NODE_ID, is defined on the Role managed by MultiRole, and has a
value of ctxt.name. This context expression represents the name used to
reach this role when the expression is evaluated. Context expressions
aren't evaluated until they are used, and hence the value of this
expression will depend on what node in the grid it is evaluated for. In
this case, it is evaluateed for ns.grid[5], and hence ctxt.name will
have a value of '5'. For each grid role created, the value of ctxt.name
will match the key used in ns.grid[key].
It's also worth noting in the two different methods used to set Vars on
namespace model roles or containers. In the first method, Vars can be
set using the keyword argument "variables"; the value must be an
iterable (list) of Var objects to set on the role. In the second method,
Vars are added to a role container with the add_variable() method, which
takes an arbitrary number of Var objects when called, separated by ','.
The add_variable() method has a return value of the role the method was
invoked on, and hence the value of VarExample.grid is still the
MultiRole instance.
Variable setting and overrides
Vars don't have to be defined when the namespace model class is defined;
they can specified as having an empty value (None in Python), and that
value can be provided later.
There are two ways to supply a missing Var value:
- The add_variables() method can be used to supply a Var to a role,
role container, or model instance after the model has been defined.
This is a "destructive" call in that if another Var with the same
name (same first parameter value) already exists on the object, it
will be replaced with the Var object in the add_variables() call.
- The add_override() method is similar to add_variables() in that it
allows a new Var to be supplied after a model instance has been
created, but unlike add_variables(), it saves the Var in an
"override" area which is searched first when a variable name is
required, leaving the original Var in tact. The override can be
subsequently cleared out and any original Var values will then be
visible.
Configuration models
--------------------
The configuration model is what instructs Actuator to do to the new
provisioned infrastructure in order to make it ready to run application
software. The configuration model has two main aspects:
1. A declaration of the tasks that need to be performed
2. A declaration of the dependencies between the tasks that will
dictate the order of performance
Together, this provides Actuator the information it needs to perform all
configuration tasks on the proper system roles in the proper order.
Declaring tasks
Tasks must be declared relative to a Namespace and its Roles; it is the
roles that inform the config model where the tasks are to ultimately be
run. In the following examples, we'll use the this simple namespace that
sets up a target role where some files are to be copied, as well as a
couple of Vars that dictate where the files will go.
~~~~ {.python}
class SimpleNamespace(NamespaceModel):
with_variables(Var("DEST", "/tmp"),
Var("PKG", "actuator"),
Var("CMD_TARGET", "127.0.0.1"))
copy_target = Role("copy_target", host_ref="!{CMD_TARGET}")
ns = SimpleNamespace()
~~~~
We've established several Vars at the model level, one which includes a
hard-coded IP to use for commands, in this case 'localhost', and a
single role that will be the target of the files we want to copy. NOTE:
Actuator uses Ansible under the covers for managing the execution of
commands over ssh, and hence for this example to work it must be run on
a *nix box that has appropriate ssh keys set up to allow for
passwordless login.
Declaring tasks is a matter of creating one or more instances of various
task classes which in many cases are Actuator analogs for Ansible
modules. In this example, we'll declare two tasks: one which will remove
any past files copied to the target (only really needed for non-dynamic