Merge from OSM SO master
[osm/SO.git] / rwprojectmano / plugins / rwprojectmano / rift / tasklets / rwprojectmano / projectmano.py
1 #
2 # Copyright 2017 RIFT.IO Inc
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
7 #
8 # http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15 #
16
17 """
18 Project Manager tasklet is responsible for managing the Projects
19 configurations required for Role Based Access Control feature.
20 """
21
22
23 import asyncio
24
25 import gi
26 gi.require_version('RwDts', '1.0')
27 gi.require_version('RwProjectManoYang', '1.0')
28 from gi.repository import (
29 RwDts as rwdts,
30 ProtobufC,
31 RwTypes,
32 RwProjectManoYang,
33 )
34
35 import rift.tasklets
36 from rift.mano.utils.project import (
37 NS_PROJECT,
38 get_add_delete_update_cfgs,
39 ProjectConfigCallbacks,
40 )
41
42
43 MANO_PROJECT_ROLES = [
44 { 'mano-role':"rw-project-mano:catalog-oper",
45 'description':("The catalog-oper Role has read permission to nsd-catalog "
46 "and vnfd-catalog under specific Projects, "
47 "as identified by /rw-project:project/rw-project:name. The "
48 "catatlog-oper Role may also have execute permission to specific "
49 "non-mutating RPCs. This Role is intended for read-only access to "
50 "catalogs under a specific project.") },
51
52 { 'mano-role':"rw-project-mano:catalog-admin",
53 'description':("The catalog-admin Role has full CRUDX permissions to vnfd and nsd "
54 "catalogs under specific Projects, as identified by "
55 "/rw-project:project/rw-project:name.") },
56
57 { 'mano-role':"rw-project-mano:lcm-oper",
58 'description':("The lcm-oper Role has read permission to the VL, VNF and NS "
59 "records within a Project. The lcm-oper Role may also have "
60 "execute permission to specific non-mutating RPCs.") },
61
62 { 'mano-role':"rw-project-mano:lcm-admin",
63 'description':("The lcm-admin Role has full CRUDX permissions to the VL, VNF "
64 "and NS records within a Project. The lcm-admin Role does "
65 "not provide general CRUDX permissions to the Project as a whole, "
66 "nor to the RIFT.ware platform in general.") },
67
68 { 'mano-role':"rw-project-mano:account-oper",
69 'description':("The account-oper Role has read permission to the VIM, SDN, VCA "
70 "and RO accounts within a Project. The account-oper Role may also have "
71 "execute permission to specific non-mutating RPCs.") },
72
73 { 'mano-role':"rw-project-mano:account-admin",
74 'description':("The account-admin Role has full CRUDX permissions to the VIM, SDN, VCA "
75 "and RO accounts within a Project. The account-admin Role does "
76 "not provide general CRUDX permissions to the Project as a whole, "
77 "nor to the RIFT.ware platform in general.") },
78 ]
79
80
81 class ProjectDtsHandler(object):
82 XPATH = "C,/{}".format(NS_PROJECT)
83
84 def __init__(self, dts, log, callbacks):
85 self._dts = dts
86 self._log = log
87 self._callbacks = callbacks
88
89 self.reg = None
90 self.projects = []
91
92 @property
93 def log(self):
94 return self._log
95
96 @property
97 def dts(self):
98 return self._dts
99
100 def add_project(self, cfg):
101 name = cfg.name
102 self.log.info("Adding project: {}".format(name))
103
104 if name not in self.projects:
105 self._callbacks.on_add_apply(name, cfg)
106 self.projects.append(name)
107 else:
108 self.log.error("Project already present: {}".
109 format(name))
110
111 def delete_project(self, name):
112 self._log.info("Deleting project: {}".format(name))
113 if name in self.projects:
114 self._callbacks.on_delete_apply(name)
115 self.projects.remove(name)
116 else:
117 self.log.error("Unrecognized project: {}".
118 format(name))
119
120 def update_project(self, cfg):
121 """ Update an existing project
122
123 Currently, we do not take any action on MANO for this,
124 so no callbacks are defined
125
126 Arguments:
127 msg - The project config message
128 """
129 name = cfg.name
130 self._log.info("Updating project: {}".format(name))
131 if name in self.projects:
132 pass
133 else:
134 self.log.error("Unrecognized project: {}".
135 format(name))
136
137 def register(self):
138 @asyncio.coroutine
139 def apply_config(dts, acg, xact, action, scratch):
140 self._log.debug("Got project apply config (xact: %s) (action: %s)", xact, action)
141
142 if xact.xact is None:
143 if action == rwdts.AppconfAction.INSTALL:
144 curr_cfg = self._reg.elements
145 for cfg in curr_cfg:
146 self._log.debug("Project being re-added after restart.")
147 self.add_project(cfg.name)
148 else:
149 # When RIFT first comes up, an INSTALL is called with the current config
150 # Since confd doesn't actally persist data this never has any data so
151 # skip this for now.
152 self._log.debug("No xact handle. Skipping apply config")
153
154 return
155
156 add_cfgs, delete_cfgs, update_cfgs = get_add_delete_update_cfgs(
157 dts_member_reg=self._reg,
158 xact=xact,
159 key_name="name",
160 )
161
162 # Handle Deletes
163 for cfg in delete_cfgs:
164 self.delete_project(cfg.name)
165
166 # Handle Adds
167 for cfg in add_cfgs:
168 self.add_project(cfg)
169
170 # Handle Updates
171 for cfg in update_cfgs:
172 self.update_project(cfg)
173
174 return RwTypes.RwStatus.SUCCESS
175
176 @asyncio.coroutine
177 def on_prepare(dts, acg, xact, xact_info, ks_path, msg, scratch):
178 """ Prepare callback from DTS for Project """
179
180 action = xact_info.query_action
181 name = msg.name
182
183 self._log.debug("Project %s on_prepare config received (action: %s): %s",
184 name, xact_info.query_action, msg)
185
186 if action in [rwdts.QueryAction.CREATE, rwdts.QueryAction.UPDATE]:
187 if name in self.projects:
188 self._log.debug("Project {} already exists. Ignore request".
189 format(name))
190
191 else:
192 self._log.debug("Project {}: Invoking on_prepare add request".
193 format(name))
194 yield from self._callbacks.on_add_prepare(name, msg)
195
196
197 elif action == rwdts.QueryAction.DELETE:
198 # Check if the entire project got deleted
199 fref = ProtobufC.FieldReference.alloc()
200 fref.goto_whole_message(msg.to_pbcm())
201 if fref.is_field_deleted():
202 if name in self.projects:
203 rc = yield from self._callbacks.on_delete_prepare(name)
204 if not rc:
205 self._log.error("Project {} should not be deleted".
206 format(name))
207 xact_info.respond_xpath(rwdts.XactRspCode.NACK)
208 return
209 else:
210 self._log.warning("Delete on unknown project: {}".
211 format(name))
212
213 else:
214 self._log.error("Action (%s) NOT SUPPORTED", action)
215 xact_info.respond_xpath(rwdts.XactRspCode.NACK)
216 return
217
218 xact_info.respond_xpath(rwdts.XactRspCode.ACK)
219
220 self._log.debug("Registering for project config using xpath: %s",
221 ProjectDtsHandler.XPATH)
222
223 acg_handler = rift.tasklets.AppConfGroup.Handler(
224 on_apply=apply_config,
225 )
226
227 with self._dts.appconf_group_create(acg_handler) as acg:
228 self._reg = acg.register(
229 xpath=ProjectDtsHandler.XPATH,
230 flags=rwdts.Flag.SUBSCRIBER | rwdts.Flag.DELTA_READY | rwdts.Flag.CACHE,
231 on_prepare=on_prepare,
232 )
233
234
235 class ProjectHandler(object):
236 def __init__(self, tasklet, project_class):
237 self._tasklet = tasklet
238 self._log = tasklet.log
239 self._log_hdl = tasklet.log_hdl
240 self._dts = tasklet.dts
241 self._loop = tasklet.loop
242 self._class = project_class
243
244 self._log.debug("Creating project config handler")
245 self.project_cfg_handler = ProjectDtsHandler(
246 self._dts, self._log,
247 ProjectConfigCallbacks(
248 on_add_apply=self.on_project_added,
249 on_add_prepare=self.on_add_prepare,
250 on_delete_apply=self.on_project_deleted,
251 on_delete_prepare=self.on_delete_prepare,
252 )
253 )
254
255 def _get_tasklet_name(self):
256 return self._tasklet.tasklet_info.instance_name
257
258 def _get_project(self, name):
259 try:
260 proj = self._tasklet.projects[name]
261 except Exception as e:
262 self._log.exception("Project {} ({})not found for tasklet {}: {}".
263 format(name, list(self._tasklet.projects.keys()),
264 self._get_tasklet_name(), e))
265 raise e
266
267 return proj
268
269 def on_project_deleted(self, name):
270 self._log.debug("Project {} deleted".format(name))
271 try:
272 self._get_project(name).deregister()
273 except Exception as e:
274 self._log.exception("Project {} deregister for {} failed: {}".
275 format(name, self._get_tasklet_name(), e))
276
277 try:
278 proj = self._tasklet.projects.pop(name)
279 del proj
280 except Exception as e:
281 self._log.exception("Project {} delete for {} failed: {}".
282 format(name, self._get_tasklet_name(), e))
283
284 def on_project_added(self, name, cfg):
285 self._log.debug("Project {} added to tasklet {}".
286 format(name, self._get_tasklet_name()))
287 self._get_project(name)._apply = True
288
289 @asyncio.coroutine
290 def on_add_prepare(self, name, msg):
291 self._log.debug("Project {} to be added to {}".
292 format(name, self._get_tasklet_name()))
293
294 try:
295 self._tasklet.projects[name] = \
296 self._class(name, self._tasklet)
297 except Exception as e:
298 self._log.exception("Project {} create for {} failed: {}".
299 format(name, self._get_tasklet_name(), e))
300
301 try:
302 yield from self._get_project(name).register()
303 except Exception as e:
304 self._log.exception("Project {} register for tasklet {} failed: {}".
305 format(name, self._get_tasklet_name(), e))
306
307 self._log.debug("Project {} added to {}".
308 format(name, self._get_tasklet_name()))
309
310 @asyncio.coroutine
311 def on_delete_prepare(self, name):
312 self._log.debug("Project {} being deleted for tasklet {}".
313 format(name, self._get_tasklet_name()))
314 rc = yield from self._get_project(name).delete_prepare()
315 return rc
316
317 def register(self):
318 self.project_cfg_handler.register()
319
320
321 class ProjectStateRolePublisher(rift.tasklets.DtsConfigPublisher):
322
323 def __init__(self, tasklet):
324 super().__init__(tasklet)
325 self.proj_state = RwProjectManoYang.YangData_RwProject_Project_ProjectState()
326 self.projects = set()
327 self.roles = MANO_PROJECT_ROLES
328
329 def get_xpath(self):
330 return "D,/rw-project:project/rw-project:project-state/rw-project-mano:mano-role"
331
332 def role_xpath(self, project, role):
333 return "/rw-project:project[rw-project:name='{}']".format(project) + \
334 "/rw-project:project-state/rw-project-mano:mano-role" + \
335 "[rw-project-mano:role='{}']".format(role['mano-role'])
336
337 def pb_role(self, role):
338 pbRole = self.proj_state.create_mano_role()
339 pbRole.role = role['mano-role']
340 pbRole.description = role['description']
341 return pbRole
342
343 def publish_roles(self, project):
344 if not project in self.projects:
345 self.projects.add(project)
346 for role in self.roles:
347 xpath = self.role_xpath(project, role)
348 pb_role = self.pb_role(role)
349 self.log.debug("publishing xpath:{}".format(xpath))
350 self._regh.update_element(xpath, pb_role)
351
352 def unpublish_roles(self, project):
353 if project in self.projects:
354 self.projects.remove(project)
355 for role in self.roles:
356 xpath = self.role_xpath(project, role)
357 self.log.debug("unpublishing xpath:{}".format(xpath))
358 self._regh.delete_element(xpath)