9ef3b342b7c3704b786ee371083924bafe415dab
[osm/riftware.git] /
1 #!/usr/bin/env python3
2
3 #    Copyright 2016 RIFT.IO Inc
4 #
5 #    Licensed under the Apache License, Version 2.0 (the "License");
6 #    you may not use this file except in compliance with the License.
7 #    You may obtain a copy of the License at
8 #
9 #        http://www.apache.org/licenses/LICENSE-2.0
10 #
11 #    Unless required by applicable law or agreed to in writing, software
12 #    distributed under the License is distributed on an "AS IS" BASIS,
13 #    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 #    See the License for the specific language governing permissions and
15 #    limitations under the License.
16
17 import argparse
18 import hashlib
19 import ipaddress
20 import itertools
21 import jujuclient
22 import logging
23 import sys
24 import time
25 import yaml
26
27
28 logging.basicConfig(filename="/tmp/rift_ns_add_corp.log", level=logging.DEBUG)
29 logger = logging.getLogger()
30
31 ch = logging.StreamHandler()
32 ch.setLevel(logging.INFO)
33
34 # create formatter and add it to the handlers
35 formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
36 ch.setFormatter(formatter)
37 logger.addHandler(ch)
38
39
40 dry_run = False
41
42 class JujuActionError(Exception):
43     pass
44
45
46 class JujuClient(object):
47     """Class for executing Juju actions """
48     def __init__(self, ip, port, user, passwd):
49         self._ip = ip
50         self._port = port
51         self._user = user
52         self._passwd = passwd
53
54         endpoint = 'wss://%s:%d' % (ip, port)
55         logger.debug("Using endpoint=%s", endpoint)
56         if dry_run:
57             return
58         self.env = jujuclient.Environment(endpoint)
59         self.env.login(passwd, user)
60
61     def get_service(self, name):
62         return self.env.get_service(name)
63
64     def _get_units(self, name):
65         """
66         Get the units associated with service
67         """
68         units = self.env.status(name)['Services'][name]['Units']
69         units = list(units.keys())
70
71         # convert to a friendly format for juju-python-client
72         units[:] = [('unit-%s' % u).replace('/', '-') for u in units]
73         return units
74
75     def exec_action(self, name, action_name, params, block=False):
76         logger.debug("execute actiion %s using params %s", action_name, params)
77         if dry_run:
78             return
79
80         actions = jujuclient.Actions(self.env)
81         results = actions.enqueue_units(self._get_units(name),
82                                         action_name,
83                                         params)
84         if not block:
85             return results
86
87         if 'error' in results['results'][0].keys():
88             raise JujuActionError("Juju action error: %s" % results['results'][0])
89
90         action = results['results'][0]['action']
91         info = actions.info([action])
92         i = 0
93         logging.debug("Initial action results: %s", results['results'][0])
94         while info['results'][0]['status'] not in ['completed', 'failed']:
95             time.sleep(1)
96             info = actions.info([action])
97
98             # break out if the action doesn't complete in 10 secs
99             i += 1
100             if i == 10:
101                 raise JujuActionError("Juju action timed out after 30 seconds")
102
103         if info['results'][0]['status'] != 'completed':
104             raise JujuActionError("Action %s failure: %s" % (action_name, info['results'][0]))
105
106         return info
107
108
109 class CharmAction(object):
110     def __init__(self, deployed_name, action_name, action_params=None):
111         self._deployed_name = deployed_name
112         self._action_name = action_name
113         self._params = action_params if action_params is not None else []
114
115     def execute(self, juju_client):
116         logger.info("Executing charm (%s) action (%s) with params (%s)",
117                      self._deployed_name, self._action_name, self._params)
118         try:
119             info = juju_client.exec_action(
120                     name=self._deployed_name,
121                     action_name=self._action_name,
122                     params=self._params,
123                     block=True
124                     )
125
126         except JujuActionError as e:
127             logger.error("Juju charm (%s) action (%s) failed: %s",
128                          self._deployed_name, self._action_name, str(e))
129             raise
130
131         logger.debug("Juju charm (%s) action (%s) success.",
132                      self._deployed_name, self._action_name)
133
134
135 class DeployedProxyCharm(object):
136     def __init__(self, juju_client, service_name, mgmt_ip=None, charm_name=None):
137         self._juju_client = juju_client
138         self.service_name = service_name
139         self.mgmt_ip = mgmt_ip
140         self.charm_name = charm_name
141
142     def do_action(self, action_name, action_params={}):
143         action = CharmAction(self.service_name, action_name, action_params)
144         action.execute(self._juju_client)
145
146
147 class SixWindPEProxyCharm(DeployedProxyCharm):
148     USER = "root"
149     PASSWD = "6windos"
150
151     def configure_interface(self, iface_name, ipv4_interface_str=None):
152         action = "configure-interface"
153         params = {'iface-name', iface_name}
154
155         if ipv4_interface_str is None:
156             # Use ipaddress module to validate ipv4 interface string
157             ip_intf = ipaddress.IPv4Interface(ipv4_interface_str)
158             params["cidr"] = ip_intf.with_prefixlen
159
160             self.do_action(action, params)
161         else:
162             self.do_action(action, params)
163
164
165     def add_corporation(self, domain_name, user_iface_name, vlan_id, corp_gw,
166                         corp_net, local_net="10.255.255.0/24", local_net_area="0"):
167         logger.debug("Add corporation called with params: %s", locals())
168
169         action = "add-corporation"
170         params = {
171                 "domain-name": domain_name,
172                 "iface-name": user_iface_name,
173                 "vlan-id": int(vlan_id),
174                 "cidr": corp_net,
175                 "area": corp_gw,
176                 "subnet-cidr":local_net,
177                 "subnet-area":local_net_area,
178                 }
179
180         self.do_action(action, params)
181
182     def connect_domains(self, domain_name, core_iface_name, local_ip, remote_ip,
183                         internal_local_ip, internal_remote_ip, tunnel_name,
184                         tunnel_key, tunnel_type="gre"):
185
186         logger.debug("Connect domains called with params: %s", locals())
187
188         action = "connect-domains"
189         params = {
190                 "domain-name": domain_name,
191                 "iface-name": core_iface_name,
192                 "tunnel-name": tunnel_name,
193                 "local-ip": local_ip,
194                 "remote-ip": remote_ip,
195                 "tunnel-key": tunnel_key,
196                 "internal-local-ip": internal_local_ip,
197                 "internal-remote-ip": internal_remote_ip,
198                 "tunnel-type":tunnel_type,
199                 }
200
201         self.do_action(action, params)
202
203
204 class PEGroupConfig(object):
205     def __init__(self, pe_group_cfg):
206         self._pe_group_cfg = pe_group_cfg
207
208     def _get_param_value(self, param_name):
209         for param in self._pe_group_cfg["parameter"]:
210             if param["name"] == param_name:
211                 return param["value"]
212
213         raise ValueError("PE param not found: %s" % param_name)
214
215     @property
216     def vlan_id(self):
217         return self._get_param_value("Vlan ID")
218
219     @property
220     def interface_name(self):
221         return self._get_param_value("Interface Name")
222
223     @property
224     def corp_network(self):
225         return self._get_param_value("Corp. Network")
226
227     @property
228     def corp_gateway(self):
229         return self._get_param_value("Corp. Gateway")
230
231
232 class AddCorporationRequest(object):
233     def __init__(self, add_corporation_rpc):
234         self._add_corporation_rpc = add_corporation_rpc
235
236     @property
237     def name(self):
238         return self._add_corporation_rpc["name"]
239
240     @property
241     def param_groups(self):
242         return self._add_corporation_rpc["parameter_group"]
243
244     @property
245     def params(self):
246         return self._add_corporation_rpc["parameter"]
247
248     @property
249     def corporation_name(self):
250         for param in self.params:
251             if param["name"] == "Corporation Name":
252                 return param["value"]
253
254         raise ValueError("Could not find 'Corporation Name' field")
255
256     @property
257     def tunnel_key(self):
258         for param in self.params:
259             if param["name"] == "Tunnel Key":
260                 return param["value"]
261
262         raise ValueError("Could not find 'Tunnel Key' field")
263
264     def get_pe_parameter_group_map(self):
265         group_name_map = {}
266         for group in self.param_groups:
267             group_name_map[group["name"]] = group
268
269         return group_name_map
270
271     def get_parameter_name_map(self):
272         name_param_map = {}
273         for param in self.params:
274             name_param_map[param["name"]] = param
275
276         return name_param_map
277
278     @classmethod
279     def from_yaml_cfg(cls, yaml_hdl):
280         config = yaml.load(yaml_hdl)
281         return cls(
282                 config["rpc_ip"],
283                 )
284
285
286 class JujuVNFConfig(object):
287     def __init__(self, vnfr_index_map, vnf_name_map, vnf_init_config_map):
288         self._vnfr_index_map = vnfr_index_map
289         self._vnf_name_map = vnf_name_map
290         self._vnf_init_config_map = vnf_name_map
291
292     def get_service_name(self, vnf_index):
293         for vnfr_id, index in self._vnfr_index_map.items():
294             if index != vnf_index:
295                 continue
296
297             return self._vnf_name_map[vnfr_id]
298
299         raise ValueError("VNF Index not found: %s" % vnf_index)
300
301     def get_vnfr_id(self, vnf_index):
302         for vnfr_id, index in self._vnfr_index_map.items():
303             if index != vnf_index:
304                 continue
305
306             return vnfr_id
307
308         raise ValueError("VNF Index not found: %s" % vnf_index)
309
310     @classmethod
311     def from_yaml_cfg(cls, yaml_hdl):
312         config = yaml.load(yaml_hdl)
313         return cls(
314                 config["vnfr_index_map"],
315                 config["unit_names"],
316                 config["init_config"],
317                 )
318
319
320 class JujuClientConfig(object):
321     def __init__(self, juju_ctrl_cfg):
322         self._juju_ctrl_cfg = juju_ctrl_cfg
323
324     @property
325     def name(self):
326         return self._juju_ctrl_cfg["name"]
327
328     @property
329     def host(self):
330         return self._juju_ctrl_cfg["host"]
331
332     @property
333     def port(self):
334         return self._juju_ctrl_cfg["port"]
335
336     @property
337     def user(self):
338         return self._juju_ctrl_cfg["user"]
339
340     @property
341     def secret(self):
342         return self._juju_ctrl_cfg["secret"]
343
344     @classmethod
345     def from_yaml_cfg(cls, yaml_hdl):
346         config = yaml.load(yaml_hdl)
347         return cls(
348                 config["config_agent"],
349                 )
350
351
352 class OSM_MWC_Demo(object):
353     VNF_INDEX_NAME_MAP = {
354             "PE1": 1,
355             "PE2": 2,
356             "PE3": 3,
357         }
358
359     CORE_PE_CONN_MAP = {
360             "PE1": {
361                 "PE2": {
362                     "ifacename": "eth1",
363                     "ip": "10.10.10.9",
364                     "mask": "30",
365                     "internal_local_ip": "10.255.255.1"
366                 },
367                 "PE3": {
368                     "ifacename": "eth2",
369                     "ip": "10.10.10.1",
370                     "mask": "30",
371                     "internal_local_ip": "10.255.255.1"
372                 },
373             },
374             "PE2": {
375                 "PE1": {
376                     "ifacename": "eth1",
377                     "ip": "10.10.10.10",
378                     "mask": "30",
379                     "internal_local_ip": "10.255.255.2"
380                 },
381                 "PE3": {
382                     "ifacename": "eth2",
383                     "ip": "10.10.10.6",
384                     "mask": "30",
385                     "internal_local_ip": "10.255.255.2"
386                 }
387             },
388             "PE3": {
389                 "PE1": {
390                     "ifacename": "eth1",
391                     "ip": "10.10.10.2",
392                     "mask": "30",
393                     "internal_local_ip": "10.255.255.3"
394                 },
395                 "PE2": {
396                     "ifacename": "eth2",
397                     "ip": "10.10.10.5",
398                     "mask": "30",
399                     "internal_local_ip": "10.255.255.3"
400                 }
401             }
402         }
403
404     @staticmethod
405     def get_pe_vnf_index(pe_name):
406         if pe_name not in OSM_MWC_Demo.VNF_INDEX_NAME_MAP:
407             raise ValueError("Could not find PE name: %s", pe_name)
408
409         return OSM_MWC_Demo.VNF_INDEX_NAME_MAP[pe_name]
410
411     @staticmethod
412     def get_src_core_iface(src_pe_name, dest_pe_name):
413         return OSM_MWC_Demo.CORE_PE_CONN_MAP[src_pe_name][dest_pe_name]["ifacename"]
414
415     @staticmethod
416     def get_local_ip(src_pe_name, dest_pe_name):
417         return OSM_MWC_Demo.CORE_PE_CONN_MAP[src_pe_name][dest_pe_name]["ip"]
418
419     @staticmethod
420     def get_remote_ip(src_pe_name, dest_pe_name):
421         return OSM_MWC_Demo.CORE_PE_CONN_MAP[dest_pe_name][src_pe_name]["ip"]
422
423     @staticmethod
424     def get_internal_local_ip(src_pe_name, dest_pe_name):
425         return OSM_MWC_Demo.CORE_PE_CONN_MAP[src_pe_name][dest_pe_name]["internal_local_ip"]
426
427     @staticmethod
428     def get_internal_remote_ip(src_pe_name, dest_pe_name):
429         return OSM_MWC_Demo.CORE_PE_CONN_MAP[dest_pe_name][src_pe_name]["internal_local_ip"]
430
431
432 def add_pe_corporation(src_pe_name, src_pe_charm, src_pe_group_cfg, corporation_name):
433     domain_name = corporation_name
434     vlan_id = src_pe_group_cfg.vlan_id
435     corp_gw = src_pe_group_cfg.corp_gateway
436     corp_net = src_pe_group_cfg.corp_network
437
438     user_iface = src_pe_group_cfg.interface_name
439
440     src_pe_charm.add_corporation(domain_name, user_iface, vlan_id, corp_gw, corp_net)
441
442
443 def connect_pe_domains(src_pe_name, src_pe_charm, dest_pe_name, corporation_name, tunnel_key):
444     domain_name = corporation_name
445     core_iface_name = OSM_MWC_Demo.get_src_core_iface(src_pe_name, dest_pe_name)
446     local_ip = OSM_MWC_Demo.get_local_ip(src_pe_name, dest_pe_name)
447     remote_ip = OSM_MWC_Demo.get_remote_ip(src_pe_name, dest_pe_name)
448     internal_local_ip = OSM_MWC_Demo.get_internal_local_ip(src_pe_name, dest_pe_name)
449     internal_remote_ip = OSM_MWC_Demo.get_internal_remote_ip(src_pe_name, dest_pe_name)
450
451
452     src_pe_idx = OSM_MWC_Demo.get_pe_vnf_index(src_pe_name)
453     dest_pe_idx = OSM_MWC_Demo.get_pe_vnf_index(dest_pe_name)
454
455     # Create a 4 digit hash of the corporation name
456     hash_object = hashlib.md5(corporation_name.encode())
457     corp_hash = hash_object.hexdigest()[-4:]
458
459     # Tunnel name is the 4 digit corporation name hash followed by
460     # src index and dest index. When there are less than 10 PE's
461     # this creates a 8 character tunnel name which is the limit.
462     tunnel_name = "".join([corp_hash, "_", str(src_pe_idx), str(dest_pe_idx)])
463
464     src_pe_charm.connect_domains(domain_name, core_iface_name, local_ip, remote_ip,
465                                  internal_local_ip, internal_remote_ip, tunnel_name,
466                                  tunnel_key)
467
468
469 def main(argv=sys.argv[1:]):
470     parser = argparse.ArgumentParser()
471     parser.add_argument("yaml_cfg_file", type=argparse.FileType('r'))
472     parser.add_argument("--dry-run", action="store_true")
473     parser.add_argument("--quiet", "-q", dest="verbose", action="store_false")
474     args = parser.parse_args()
475     if args.verbose:
476         ch.setLevel(logging.DEBUG)
477
478     global dry_run
479     dry_run = args.dry_run
480
481     yaml_str = args.yaml_cfg_file.read()
482
483     juju_cfg = JujuClientConfig.from_yaml_cfg(yaml_str)
484     juju_client = JujuClient(juju_cfg.host, juju_cfg.port, juju_cfg.user, juju_cfg.secret)
485
486     juju_vnf_config = JujuVNFConfig.from_yaml_cfg(yaml_str)
487
488     rpc_request = AddCorporationRequest.from_yaml_cfg(yaml_str)
489     pe_param_group_map = rpc_request.get_pe_parameter_group_map()
490
491     pe_name_charm_map = {}
492     for pe_name, pe_group_cfg in pe_param_group_map.items():
493         # The PE name (i.e. PE1) must be in the parameter group name so we can correlate
494         # to an actual VNF in the descriptor.
495         pe_vnf_index = OSM_MWC_Demo.get_pe_vnf_index(pe_name)
496
497         # Get the deployed VNFR charm service name
498         pe_charm_service_name = juju_vnf_config.get_service_name(pe_vnf_index)
499
500         pe_name_charm_map[pe_name] = SixWindPEProxyCharm(juju_client, pe_charm_service_name)
501
502     # At this point we have SixWindPEProxyCharm() instances for each PE and each
503     # PE param group configuration.
504     for src_pe_name in pe_param_group_map:
505         add_pe_corporation(
506                 src_pe_name=src_pe_name,
507                 src_pe_charm=pe_name_charm_map[src_pe_name],
508                 src_pe_group_cfg=PEGroupConfig(pe_param_group_map[src_pe_name]),
509                 corporation_name=rpc_request.corporation_name
510                 )
511
512     # Create a permutation of all PE's involved in this topology and connect
513     # them together by creating tunnels with matching keys
514     for src_pe_name, dest_pe_name in itertools.permutations(pe_name_charm_map, 2):
515         connect_pe_domains(
516                 src_pe_name=src_pe_name,
517                 src_pe_charm=pe_name_charm_map[src_pe_name],
518                 dest_pe_name=dest_pe_name,
519                 corporation_name=rpc_request.corporation_name,
520                 tunnel_key=rpc_request.tunnel_key,
521                 )
522
523 if __name__ == "__main__":
524     try:
525         main()
526     except Exception as e:
527         logger.exception("Caught exception when executing add_corporation ns")
528         raise