0a2747cc5358c915b451d75e5b35670007432d69
[tests.git] / system / TestBonding.py
1 """
2 Utilities to create a setup made of 2 different builds
3 read : 2 different node flavours 
4 so that each myplc knows about the nodeflavour/slicefamily supported
5 by the other one
6 ---
7 This would be the basics for running tests on multi-node myplc, 
8 in particular for node upgrades
9 """
10
11 #################### WARNING
12
13 # this feature relies on a few assumptions that need to be taken care of
14 # more or less manually; this is based on the onelab.eu setup
15
16 # (*) the build host is expected to have /root/git-build.sh reasonably up-to-date
17 # with our build module, so we can locate partial-repo.sh
18 # this utility needs to be run on the build host so we can point at a PARTIAL-RPMS
19 # sub-repo that exposes the
20 # bootcd/bootstraps/ and the like rpms from one flavour to another
21
22 # a utility to create a bonding_plc_spec from
23 # a plc_spec and just a buildname
24
25 def onelab_bonding_spec (buildname):
26
27     # essentially generic ..
28     buildname      = buildname
29     
30     # visit the other build's test directory to figure its characteristics
31     with open ("../{}/arg-fcdistro".format(buildname)) as input:
32         fcdistro   = input.read().strip()
33     with open ("../{}/arg-pldistro".format(buildname)) as input:
34         pldistro   = input.read().strip()
35     with open ("../{}/arg-ips-bplc".format(buildname)) as input:
36         plc_box    = input.read().strip().split()[0]
37     # e.g. http://build.onelab.eu/onelab//2015.03.15--f14/RPMS/x86_64
38     with open ("../{}/arg-arch-rpms-url".format(buildname)) as input:
39         arch_rpms_url = input.read().strip()
40     arch           = arch_rpms_url.split('/')[-1]
41     build_www_host = arch_rpms_url.split('/')[2]
42     base_url       = arch_rpms_url.replace("RPMS/{}".format(arch), "PARTIAL-RPMS")
43         
44     # onelab specifics
45     build_www_git = '/root/git-build/'
46     build_www_dir  = '/build/{}/{}'.format(pldistro, buildname)
47     
48     return locals()
49
50 ####################
51 import os, os.path
52 import socket
53
54 import utils
55 from TestSsh import TestSsh
56
57 ####################
58 class TestBonding(object):
59
60     """
61     Holds details about a 'bonding' build
62     so we can configure the local myplc (test_plc)
63     for multi-flavour nodes and slices
64     options is a TestMain options
65
66     details for a bonding node (like hostname and IP) are
67     computed from the underlying Substrate object and 
68     stored in arg-bonding-{buildname}
69     """
70     
71     def __init__(self, test_plc, bonding_spec, substrate, options):
72         """
73         test_plc is one local TestPlc instance
74         bonding_spec is a dictionary that gives details on
75         the build we want to be bonding with
76         """
77         # the local build & plc is described in options
78         # the bonding build is described in bonding_spec
79         self.test_plc = test_plc
80         self.bonding_spec = bonding_spec
81         self.substrate = substrate
82         self.options = options
83         # a little hacky : minimal provisioning and modify plc_spec on the fly
84         self.provision()
85     
86     def nodefamily(self):
87         return "{pldistro}-{fcdistro}-{arch}".format(**self.bonding_spec)
88         
89     #################### provisioning
90     # store only hostname so it's either to set this manually
91     def persistent_name(self):
92         return "arg-bonding-{}".format(self.bonding_spec['buildname'])
93     def persistent_store(self):
94         with open(self.persistent_name(),'w') as f:
95             f.write("{}\n".format(self.vnode_hostname))
96     def persistent_load(self):
97         try:
98             with open(self.persistent_name()) as f:
99                 self.vnode_hostname = f.read().strip().split()[0]
100                 self.vnode_ip = socket.gethostbyname(self.vnode_hostname)
101             return True
102         except:
103             return False
104
105     def provision(self):
106         # locate the first node in our own spec
107         site_spec = self.test_plc.plc_spec['sites'][0]
108         node_spec = site_spec['nodes'][0]
109         # find a free IP for node
110         if self.persistent_load():
111             print("Re-using bonding nodes attributes from {}".format(self.persistent_name()))
112         else:
113             print("Sensing for an avail. IP (Could not load from {})".format(self.persistent_name()))
114             vnode_pool = self.substrate.vnode_pool
115             vnode_pool.sense()
116             try:
117                 hostname, mac = vnode_pool.next_free()
118                 self.vnode_hostname = self.substrate.fqdn(hostname)
119                 self.vnode_ip = vnode_pool.get_ip(hostname)
120                 self.vnode_mac = mac
121                 self.persistent_store()
122             except:
123                 raise Exception("Cannot provision bonding node")
124
125         print("Bonding on node {} - {}".format(self.vnode_hostname, self.vnode_ip))
126
127         # implement the node on another IP
128         node_spec['node_fields']['hostname'] = self.vnode_hostname
129         node_spec['interface_fields']['ip'] = self.vnode_ip
130         # with the node flavour that goes with bonding plc
131         for tag in ['arch', 'fcdistro', 'pldistro']:
132             node_spec['tags'][tag] = self.bonding_spec[tag]
133         # do not use plain bootstrapfs
134         del node_spec['tags']['plain-bootstrapfs']
135
136     #################### steps
137     def init_partial(self):
138         """
139         runs partial-repo.sh for the bonding build
140         this action takes place on the build host
141         """
142         test_ssh = TestSsh (self.bonding_spec['build_www_host'])
143         command = "{build_www_git}/partial-repo.sh -i {build_www_dir}".\
144                   format(**self.bonding_spec)
145                          
146         return test_ssh.run (command, dry_run = self.options.dry_run) == 0
147         
148
149     def add_yum(self):
150         """
151         creates a separate yum.repo file in the myplc box
152         where our own build runs, and that points at the partial
153         repo for the bonding build
154         """
155
156         # create a .repo file locally
157         yumrepo_contents = """
158 [{buildname}]
159 name=Partial repo from bonding build {buildname}
160 baseurl={base_url}
161 enabled=1
162 gpgcheck=0
163 """.format(**self.bonding_spec)
164
165         yumrepo_local = '{buildname}-partial.repo'.\
166                         format(**self.bonding_spec)
167         with open(yumrepo_local, 'w') as yumrepo_file:
168             yumrepo_file.write(yumrepo_contents)
169         utils.header("(Over)wrote {}".format(yumrepo_local))
170
171         # push onto our myplc instance
172         test_ssh = TestSsh (self.test_plc.vserverip)
173
174         yumrepo_remote = '/etc/yum.repos.d/{bonding_buildname}-partial.repo'.\
175                          format(bonding_buildname = self.bonding_spec['buildname'])
176
177         if test_ssh.copy_abs (yumrepo_local, yumrepo_remote,
178                               dry_run=self.options.dry_run) != 0:
179             return False
180
181         # xxx TODO looks like drupal also needs to be excluded
182         # from the 2 entries in building.repo
183         # otherwise subsequent yum update calls will fail
184
185         return True
186         
187     def install_rpms(self):
188         """
189         once the 2 operations above have been performed, we can 
190         actually install the various rpms that provide support for the 
191         nodeflavour/slicefamily offered byt the bonding build to our own build
192         """
193
194         test_ssh = TestSsh (self.test_plc.vserverip)
195         
196         command1 = "yum -y update --exclude drupal"
197         if test_ssh.run (command1, dry_run = self.options.dry_run) != 0:
198             return False
199
200         nodefamily = self.nodefamily()
201         extra_list = [ 'bootcd', 'nodeimage', 'noderepo' ]
202
203         extra_rpms = [ "{}-{}".format(rpm, nodefamily) for rpm in extra_list]
204
205         command2 = "yum -y install " + " ".join(extra_rpms) 
206         if test_ssh.run (command2, dry_run = self.options.dry_run) != 0:
207             return False
208
209         command3 = "/etc/plc.d/packages force"
210         if test_ssh.run (command3, dry_run = self.options.dry_run) != 0:
211             return False
212
213         return True
214
215 ### probably obsolete already    
216 if __name__ == '__main__':
217
218     from TestPlc import TestPlc    
219
220     from config_default import sample_test_plc_spec
221     test_plc_spec = sample_test_plc_spec()
222     test_plc = TestPlc (test_plc_spec)
223     test_plc.show()
224
225     print(test_plc.host_box)
226
227     from argparse import ArgumentParser
228     parser = ArgumentParser()
229     parser.add_argument ("-n", "--dry-run", dest='dry_run', default=False,
230                          action='store_true', help="dry run")
231     parser.add_argument ("build_name")
232     args = parser.parse_args()
233
234     test_bonding = TestBonding (test_plc,
235                                 onelab_bonding_spec(args.build_name),
236                                 dry_run = args.dry_run)
237
238     test_bonding.bond ()
239