(2.10) Allow instances to obtain externally reserved IPs
[ganeti-local] / lib / cmdlib / network.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013 Google Inc.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 # 02110-1301, USA.
20
21
22 """Logical units dealing with networks."""
23
24 from ganeti import constants
25 from ganeti import errors
26 from ganeti import locking
27 from ganeti import network
28 from ganeti import objects
29 from ganeti import qlang
30 from ganeti import query
31 from ganeti import utils
32 from ganeti.cmdlib.base import LogicalUnit, NoHooksLU, QueryBase
33 from ganeti.cmdlib.common import ShareAll
34
35
36 def _BuildNetworkHookEnv(name, subnet, gateway, network6, gateway6,
37                          mac_prefix, tags):
38   """Builds network related env variables for hooks
39
40   This builds the hook environment from individual variables.
41
42   @type name: string
43   @param name: the name of the network
44   @type subnet: string
45   @param subnet: the ipv4 subnet
46   @type gateway: string
47   @param gateway: the ipv4 gateway
48   @type network6: string
49   @param network6: the ipv6 subnet
50   @type gateway6: string
51   @param gateway6: the ipv6 gateway
52   @type mac_prefix: string
53   @param mac_prefix: the mac_prefix
54   @type tags: list
55   @param tags: the tags of the network
56
57   """
58   env = {}
59   if name:
60     env["NETWORK_NAME"] = name
61   if subnet:
62     env["NETWORK_SUBNET"] = subnet
63   if gateway:
64     env["NETWORK_GATEWAY"] = gateway
65   if network6:
66     env["NETWORK_SUBNET6"] = network6
67   if gateway6:
68     env["NETWORK_GATEWAY6"] = gateway6
69   if mac_prefix:
70     env["NETWORK_MAC_PREFIX"] = mac_prefix
71   if tags:
72     env["NETWORK_TAGS"] = " ".join(tags)
73
74   return env
75
76
77 class LUNetworkAdd(LogicalUnit):
78   """Logical unit for creating networks.
79
80   """
81   HPATH = "network-add"
82   HTYPE = constants.HTYPE_NETWORK
83   REQ_BGL = False
84
85   def BuildHooksNodes(self):
86     """Build hooks nodes.
87
88     """
89     mn = self.cfg.GetMasterNode()
90     return ([mn], [mn])
91
92   def CheckArguments(self):
93     if self.op.mac_prefix:
94       self.op.mac_prefix = \
95         utils.NormalizeAndValidateThreeOctetMacPrefix(self.op.mac_prefix)
96
97   def ExpandNames(self):
98     self.network_uuid = self.cfg.GenerateUniqueID(self.proc.GetECId())
99
100     if self.op.conflicts_check:
101       self.share_locks[locking.LEVEL_NODE] = 1
102       self.share_locks[locking.LEVEL_NODE_ALLOC] = 1
103       self.needed_locks = {
104         locking.LEVEL_NODE: locking.ALL_SET,
105         locking.LEVEL_NODE_ALLOC: locking.ALL_SET,
106         }
107     else:
108       self.needed_locks = {}
109
110     self.add_locks[locking.LEVEL_NETWORK] = self.network_uuid
111
112   def CheckPrereq(self):
113     if self.op.network is None:
114       raise errors.OpPrereqError("Network must be given",
115                                  errors.ECODE_INVAL)
116
117     try:
118       existing_uuid = self.cfg.LookupNetwork(self.op.network_name)
119     except errors.OpPrereqError:
120       pass
121     else:
122       raise errors.OpPrereqError("Desired network name '%s' already exists as a"
123                                  " network (UUID: %s)" %
124                                  (self.op.network_name, existing_uuid),
125                                  errors.ECODE_EXISTS)
126
127     # Check tag validity
128     for tag in self.op.tags:
129       objects.TaggableObject.ValidateTag(tag)
130
131   def BuildHooksEnv(self):
132     """Build hooks env.
133
134     """
135     args = {
136       "name": self.op.network_name,
137       "subnet": self.op.network,
138       "gateway": self.op.gateway,
139       "network6": self.op.network6,
140       "gateway6": self.op.gateway6,
141       "mac_prefix": self.op.mac_prefix,
142       "tags": self.op.tags,
143       }
144     return _BuildNetworkHookEnv(**args) # pylint: disable=W0142
145
146   def Exec(self, feedback_fn):
147     """Add the ip pool to the cluster.
148
149     """
150     nobj = objects.Network(name=self.op.network_name,
151                            network=self.op.network,
152                            gateway=self.op.gateway,
153                            network6=self.op.network6,
154                            gateway6=self.op.gateway6,
155                            mac_prefix=self.op.mac_prefix,
156                            uuid=self.network_uuid)
157     # Initialize the associated address pool
158     try:
159       pool = network.AddressPool.InitializeNetwork(nobj)
160     except errors.AddressPoolError, err:
161       raise errors.OpExecError("Cannot create IP address pool for network"
162                                " '%s': %s" % (self.op.network_name, err))
163
164     # Check if we need to reserve the nodes and the cluster master IP
165     # These may not be allocated to any instances in routed mode, as
166     # they wouldn't function anyway.
167     if self.op.conflicts_check:
168       for node in self.cfg.GetAllNodesInfo().values():
169         for ip in [node.primary_ip, node.secondary_ip]:
170           try:
171             if pool.Contains(ip):
172               pool.Reserve(ip, external=True)
173               self.LogInfo("Reserved IP address of node '%s' (%s)",
174                            node.name, ip)
175           except errors.AddressPoolError, err:
176             self.LogWarning("Cannot reserve IP address '%s' of node '%s': %s",
177                             ip, node.name, err)
178
179       master_ip = self.cfg.GetClusterInfo().master_ip
180       try:
181         if pool.Contains(master_ip):
182           pool.Reserve(master_ip, external=True)
183           self.LogInfo("Reserved cluster master IP address (%s)", master_ip)
184       except errors.AddressPoolError, err:
185         self.LogWarning("Cannot reserve cluster master IP address (%s): %s",
186                         master_ip, err)
187
188     if self.op.add_reserved_ips:
189       for ip in self.op.add_reserved_ips:
190         try:
191           pool.Reserve(ip, external=True)
192         except errors.AddressPoolError, err:
193           raise errors.OpExecError("Cannot reserve IP address '%s': %s" %
194                                    (ip, err))
195
196     if self.op.tags:
197       for tag in self.op.tags:
198         nobj.AddTag(tag)
199
200     self.cfg.AddNetwork(nobj, self.proc.GetECId(), check_uuid=False)
201     del self.remove_locks[locking.LEVEL_NETWORK]
202
203
204 class LUNetworkRemove(LogicalUnit):
205   HPATH = "network-remove"
206   HTYPE = constants.HTYPE_NETWORK
207   REQ_BGL = False
208
209   def ExpandNames(self):
210     self.network_uuid = self.cfg.LookupNetwork(self.op.network_name)
211
212     self.share_locks[locking.LEVEL_NODEGROUP] = 1
213     self.needed_locks = {
214       locking.LEVEL_NETWORK: [self.network_uuid],
215       locking.LEVEL_NODEGROUP: locking.ALL_SET,
216       }
217
218   def CheckPrereq(self):
219     """Check prerequisites.
220
221     This checks that the given network name exists as a network, that is
222     empty (i.e., contains no nodes), and that is not the last group of the
223     cluster.
224
225     """
226     # Verify that the network is not conncted.
227     node_groups = [group.name
228                    for group in self.cfg.GetAllNodeGroupsInfo().values()
229                    if self.network_uuid in group.networks]
230
231     if node_groups:
232       self.LogWarning("Network '%s' is connected to the following"
233                       " node groups: %s" %
234                       (self.op.network_name,
235                        utils.CommaJoin(utils.NiceSort(node_groups))))
236       raise errors.OpPrereqError("Network still connected", errors.ECODE_STATE)
237
238   def BuildHooksEnv(self):
239     """Build hooks env.
240
241     """
242     return {
243       "NETWORK_NAME": self.op.network_name,
244       }
245
246   def BuildHooksNodes(self):
247     """Build hooks nodes.
248
249     """
250     mn = self.cfg.GetMasterNode()
251     return ([mn], [mn])
252
253   def Exec(self, feedback_fn):
254     """Remove the network.
255
256     """
257     try:
258       self.cfg.RemoveNetwork(self.network_uuid)
259     except errors.ConfigurationError:
260       raise errors.OpExecError("Network '%s' with UUID %s disappeared" %
261                                (self.op.network_name, self.network_uuid))
262
263
264 class LUNetworkSetParams(LogicalUnit):
265   """Modifies the parameters of a network.
266
267   """
268   HPATH = "network-modify"
269   HTYPE = constants.HTYPE_NETWORK
270   REQ_BGL = False
271
272   def CheckArguments(self):
273     if (self.op.gateway and
274         (self.op.add_reserved_ips or self.op.remove_reserved_ips)):
275       raise errors.OpPrereqError("Cannot modify gateway and reserved ips"
276                                  " at once", errors.ECODE_INVAL)
277
278   def ExpandNames(self):
279     self.network_uuid = self.cfg.LookupNetwork(self.op.network_name)
280
281     self.needed_locks = {
282       locking.LEVEL_NETWORK: [self.network_uuid],
283       }
284
285   def CheckPrereq(self):
286     """Check prerequisites.
287
288     """
289     self.network = self.cfg.GetNetwork(self.network_uuid)
290     self.gateway = self.network.gateway
291     self.mac_prefix = self.network.mac_prefix
292     self.network6 = self.network.network6
293     self.gateway6 = self.network.gateway6
294     self.tags = self.network.tags
295
296     self.pool = network.AddressPool(self.network)
297
298     if self.op.gateway:
299       if self.op.gateway == constants.VALUE_NONE:
300         self.gateway = None
301       else:
302         self.gateway = self.op.gateway
303         if self.pool.IsReserved(self.gateway):
304           raise errors.OpPrereqError("Gateway IP address '%s' is already"
305                                      " reserved" % self.gateway,
306                                      errors.ECODE_STATE)
307
308     if self.op.mac_prefix:
309       if self.op.mac_prefix == constants.VALUE_NONE:
310         self.mac_prefix = None
311       else:
312         self.mac_prefix = \
313           utils.NormalizeAndValidateThreeOctetMacPrefix(self.op.mac_prefix)
314
315     if self.op.gateway6:
316       if self.op.gateway6 == constants.VALUE_NONE:
317         self.gateway6 = None
318       else:
319         self.gateway6 = self.op.gateway6
320
321     if self.op.network6:
322       if self.op.network6 == constants.VALUE_NONE:
323         self.network6 = None
324       else:
325         self.network6 = self.op.network6
326
327   def BuildHooksEnv(self):
328     """Build hooks env.
329
330     """
331     args = {
332       "name": self.op.network_name,
333       "subnet": self.network.network,
334       "gateway": self.gateway,
335       "network6": self.network6,
336       "gateway6": self.gateway6,
337       "mac_prefix": self.mac_prefix,
338       "tags": self.tags,
339       }
340     return _BuildNetworkHookEnv(**args) # pylint: disable=W0142
341
342   def BuildHooksNodes(self):
343     """Build hooks nodes.
344
345     """
346     mn = self.cfg.GetMasterNode()
347     return ([mn], [mn])
348
349   def Exec(self, feedback_fn):
350     """Modifies the network.
351
352     """
353     #TODO: reserve/release via temporary reservation manager
354     #      extend cfg.ReserveIp/ReleaseIp with the external flag
355     if self.op.gateway:
356       if self.gateway == self.network.gateway:
357         self.LogWarning("Gateway is already %s", self.gateway)
358       else:
359         if self.gateway:
360           self.pool.Reserve(self.gateway, external=True)
361         if self.network.gateway:
362           self.pool.Release(self.network.gateway, external=True)
363         self.network.gateway = self.gateway
364
365     if self.op.add_reserved_ips:
366       for ip in self.op.add_reserved_ips:
367         try:
368           self.pool.Reserve(ip, external=True)
369         except errors.AddressPoolError, err:
370           self.LogWarning("Cannot reserve IP address %s: %s", ip, err)
371
372     if self.op.remove_reserved_ips:
373       for ip in self.op.remove_reserved_ips:
374         if ip == self.network.gateway:
375           self.LogWarning("Cannot unreserve Gateway's IP")
376           continue
377         try:
378           self.pool.Release(ip, external=True)
379         except errors.AddressPoolError, err:
380           self.LogWarning("Cannot release IP address %s: %s", ip, err)
381
382     if self.op.mac_prefix:
383       self.network.mac_prefix = self.mac_prefix
384
385     if self.op.network6:
386       self.network.network6 = self.network6
387
388     if self.op.gateway6:
389       self.network.gateway6 = self.gateway6
390
391     self.pool.Validate()
392
393     self.cfg.Update(self.network, feedback_fn)
394
395
396 class NetworkQuery(QueryBase):
397   FIELDS = query.NETWORK_FIELDS
398
399   def ExpandNames(self, lu):
400     lu.needed_locks = {}
401     lu.share_locks = ShareAll()
402
403     self.do_locking = self.use_locking
404
405     all_networks = lu.cfg.GetAllNetworksInfo()
406     name_to_uuid = dict((n.name, n.uuid) for n in all_networks.values())
407
408     if self.names:
409       missing = []
410       self.wanted = []
411
412       for name in self.names:
413         if name in name_to_uuid:
414           self.wanted.append(name_to_uuid[name])
415         else:
416           missing.append(name)
417
418       if missing:
419         raise errors.OpPrereqError("Some networks do not exist: %s" % missing,
420                                    errors.ECODE_NOENT)
421     else:
422       self.wanted = locking.ALL_SET
423
424     if self.do_locking:
425       lu.needed_locks[locking.LEVEL_NETWORK] = self.wanted
426       if query.NETQ_INST in self.requested_data:
427         lu.needed_locks[locking.LEVEL_INSTANCE] = locking.ALL_SET
428       if query.NETQ_GROUP in self.requested_data:
429         lu.needed_locks[locking.LEVEL_NODEGROUP] = locking.ALL_SET
430
431   def DeclareLocks(self, lu, level):
432     pass
433
434   def _GetQueryData(self, lu):
435     """Computes the list of networks and their attributes.
436
437     """
438     all_networks = lu.cfg.GetAllNetworksInfo()
439
440     network_uuids = self._GetNames(lu, all_networks.keys(),
441                                    locking.LEVEL_NETWORK)
442
443     do_instances = query.NETQ_INST in self.requested_data
444     do_groups = query.NETQ_GROUP in self.requested_data
445
446     network_to_instances = None
447     network_to_groups = None
448
449     # For NETQ_GROUP, we need to map network->[groups]
450     if do_groups:
451       all_groups = lu.cfg.GetAllNodeGroupsInfo()
452       network_to_groups = dict((uuid, []) for uuid in network_uuids)
453       for _, group in all_groups.iteritems():
454         for net_uuid in network_uuids:
455           netparams = group.networks.get(net_uuid, None)
456           if netparams:
457             info = (group.name, netparams[constants.NIC_MODE],
458                     netparams[constants.NIC_LINK])
459
460             network_to_groups[net_uuid].append(info)
461
462     if do_instances:
463       all_instances = lu.cfg.GetAllInstancesInfo()
464       network_to_instances = dict((uuid, []) for uuid in network_uuids)
465       for instance in all_instances.values():
466         for nic in instance.nics:
467           if nic.network in network_uuids:
468             network_to_instances[nic.network].append(instance.name)
469             break
470
471     if query.NETQ_STATS in self.requested_data:
472       stats = \
473         dict((uuid,
474               self._GetStats(network.AddressPool(all_networks[uuid])))
475              for uuid in network_uuids)
476     else:
477       stats = None
478
479     return query.NetworkQueryData([all_networks[uuid]
480                                    for uuid in network_uuids],
481                                    network_to_groups,
482                                    network_to_instances,
483                                    stats)
484
485   @staticmethod
486   def _GetStats(pool):
487     """Returns statistics for a network address pool.
488
489     """
490     return {
491       "free_count": pool.GetFreeCount(),
492       "reserved_count": pool.GetReservedCount(),
493       "map": pool.GetMap(),
494       "external_reservations":
495         utils.CommaJoin(pool.GetExternalReservations()),
496       }
497
498
499 class LUNetworkQuery(NoHooksLU):
500   """Logical unit for querying networks.
501
502   """
503   REQ_BGL = False
504
505   def CheckArguments(self):
506     self.nq = NetworkQuery(qlang.MakeSimpleFilter("name", self.op.names),
507                             self.op.output_fields, self.op.use_locking)
508
509   def ExpandNames(self):
510     self.nq.ExpandNames(self)
511
512   def Exec(self, feedback_fn):
513     return self.nq.OldStyleQuery(self)
514
515
516 def _FmtNetworkConflict(details):
517   """Utility for L{_NetworkConflictCheck}.
518
519   """
520   return utils.CommaJoin("nic%s/%s" % (idx, ipaddr)
521                          for (idx, ipaddr) in details)
522
523
524 def _NetworkConflictCheck(lu, check_fn, action, instances):
525   """Checks for network interface conflicts with a network.
526
527   @type lu: L{LogicalUnit}
528   @type check_fn: callable receiving one parameter (L{objects.NIC}) and
529     returning boolean
530   @param check_fn: Function checking for conflict
531   @type action: string
532   @param action: Part of error message (see code)
533   @raise errors.OpPrereqError: If conflicting IP addresses are found.
534
535   """
536   conflicts = []
537
538   for (_, instance) in lu.cfg.GetMultiInstanceInfo(instances):
539     instconflicts = [(idx, nic.ip)
540                      for (idx, nic) in enumerate(instance.nics)
541                      if check_fn(nic)]
542
543     if instconflicts:
544       conflicts.append((instance.name, instconflicts))
545
546   if conflicts:
547     lu.LogWarning("IP addresses from network '%s', which is about to %s"
548                   " node group '%s', are in use: %s" %
549                   (lu.network_name, action, lu.group.name,
550                    utils.CommaJoin(("%s: %s" %
551                                     (name, _FmtNetworkConflict(details)))
552                                    for (name, details) in conflicts)))
553
554     raise errors.OpPrereqError("Conflicting IP addresses found; "
555                                " remove/modify the corresponding network"
556                                " interfaces", errors.ECODE_STATE)
557
558
559 class LUNetworkConnect(LogicalUnit):
560   """Connect a network to a nodegroup
561
562   """
563   HPATH = "network-connect"
564   HTYPE = constants.HTYPE_NETWORK
565   REQ_BGL = False
566
567   def ExpandNames(self):
568     self.network_name = self.op.network_name
569     self.group_name = self.op.group_name
570     self.network_mode = self.op.network_mode
571     self.network_link = self.op.network_link
572
573     self.network_uuid = self.cfg.LookupNetwork(self.network_name)
574     self.group_uuid = self.cfg.LookupNodeGroup(self.group_name)
575
576     self.needed_locks = {
577       locking.LEVEL_NODEGROUP: [self.group_uuid],
578       }
579
580     if self.op.conflicts_check:
581       self.needed_locks[locking.LEVEL_INSTANCE] = locking.ALL_SET
582       self.needed_locks[locking.LEVEL_NETWORK] = [self.network_uuid]
583       self.share_locks[locking.LEVEL_NETWORK] = 1
584       self.share_locks[locking.LEVEL_INSTANCE] = 1
585
586   def DeclareLocks(self, level):
587     pass
588
589   def BuildHooksEnv(self):
590     ret = {
591       "GROUP_NAME": self.group_name,
592       "GROUP_NETWORK_MODE": self.network_mode,
593       "GROUP_NETWORK_LINK": self.network_link,
594       }
595     return ret
596
597   def BuildHooksNodes(self):
598     nodes = self.cfg.GetNodeGroup(self.group_uuid).members
599     return (nodes, nodes)
600
601   def CheckPrereq(self):
602     owned_groups = frozenset(self.owned_locks(locking.LEVEL_NODEGROUP))
603
604     assert self.group_uuid in owned_groups
605
606     # Check if locked instances are still correct
607     owned_instances = frozenset(self.owned_locks(locking.LEVEL_INSTANCE))
608
609     self.netparams = {
610       constants.NIC_MODE: self.network_mode,
611       constants.NIC_LINK: self.network_link,
612       }
613     objects.NIC.CheckParameterSyntax(self.netparams)
614
615     self.group = self.cfg.GetNodeGroup(self.group_uuid)
616     #if self.network_mode == constants.NIC_MODE_BRIDGED:
617     #  _CheckNodeGroupBridgesExist(self, self.network_link, self.group_uuid)
618     self.connected = False
619     if self.network_uuid in self.group.networks:
620       self.LogWarning("Network '%s' is already mapped to group '%s'" %
621                       (self.network_name, self.group.name))
622       self.connected = True
623
624     # check only if not already connected
625     elif self.op.conflicts_check:
626       pool = network.AddressPool(self.cfg.GetNetwork(self.network_uuid))
627
628       _NetworkConflictCheck(self, lambda nic: pool.Contains(nic.ip),
629                             "connect to", owned_instances)
630
631   def Exec(self, feedback_fn):
632     # Connect the network and update the group only if not already connected
633     if not self.connected:
634       self.group.networks[self.network_uuid] = self.netparams
635       self.cfg.Update(self.group, feedback_fn)
636
637
638 class LUNetworkDisconnect(LogicalUnit):
639   """Disconnect a network to a nodegroup
640
641   """
642   HPATH = "network-disconnect"
643   HTYPE = constants.HTYPE_NETWORK
644   REQ_BGL = False
645
646   def ExpandNames(self):
647     self.network_name = self.op.network_name
648     self.group_name = self.op.group_name
649
650     self.network_uuid = self.cfg.LookupNetwork(self.network_name)
651     self.group_uuid = self.cfg.LookupNodeGroup(self.group_name)
652
653     self.needed_locks = {
654       locking.LEVEL_INSTANCE: locking.ALL_SET,
655       locking.LEVEL_NODEGROUP: [self.group_uuid],
656       }
657     self.share_locks[locking.LEVEL_INSTANCE] = 1
658
659   def DeclareLocks(self, level):
660     pass
661
662   def BuildHooksEnv(self):
663     ret = {
664       "GROUP_NAME": self.group_name,
665       }
666     return ret
667
668   def BuildHooksNodes(self):
669     nodes = self.cfg.GetNodeGroup(self.group_uuid).members
670     return (nodes, nodes)
671
672   def CheckPrereq(self):
673     owned_groups = frozenset(self.owned_locks(locking.LEVEL_NODEGROUP))
674
675     assert self.group_uuid in owned_groups
676
677     # Check if locked instances are still correct
678     owned_instances = frozenset(self.owned_locks(locking.LEVEL_INSTANCE))
679
680     self.group = self.cfg.GetNodeGroup(self.group_uuid)
681     self.connected = True
682     if self.network_uuid not in self.group.networks:
683       self.LogWarning("Network '%s' is not mapped to group '%s'",
684                       self.network_name, self.group.name)
685       self.connected = False
686
687     # We need this check only if network is not already connected
688     else:
689       _NetworkConflictCheck(self, lambda nic: nic.network == self.network_uuid,
690                             "disconnect from", owned_instances)
691
692   def Exec(self, feedback_fn):
693     # Disconnect the network and update the group only if network is connected
694     if self.connected:
695       del self.group.networks[self.network_uuid]
696       self.cfg.Update(self.group, feedback_fn)