diff --git a/qubes/tests/vm/init.py b/qubes/tests/vm/init.py
index d5439a4c..e36ea81d 100644
--- a/qubes/tests/vm/init.py
+++ b/qubes/tests/vm/init.py
@@ -67,7 +67,7 @@ class TC_10_BaseVM(qubes.tests.QubesTestCase):
- tagvalue
+
@@ -101,7 +101,7 @@ class TC_10_BaseVM(qubes.tests.QubesTestCase):
self.assertEqual(vm.testprop, 'testvalue')
self.assertEqual(vm.testlabel, 'label-1')
self.assertEqual(vm.defaultprop, 'defaultvalue')
- self.assertEqual(vm.tags, {'testtag': 'tagvalue'})
+ self.assertEqual(vm.tags, {'testtag'})
self.assertEqual(vm.features, {
'testfeature_empty': '',
'testfeature_aqq': 'aqq',
diff --git a/qubes/vm/__init__.py b/qubes/vm/__init__.py
index 64056633..e5499b57 100644
--- a/qubes/vm/__init__.py
+++ b/qubes/vm/__init__.py
@@ -28,6 +28,7 @@
import datetime
import os
import re
+import string
import subprocess
import sys
import xml.parsers.expat
@@ -159,6 +160,82 @@ class Features(dict):
return default
+class Tags(set):
+ '''Manager of the tags.
+
+ Tags are simple: tag either can be present on qube or not. Tag is a
+ simple string consisting of ASCII alphanumeric characters, plus `_` and
+ `-`.
+
+ This class inherits from set, but has most of the methods that manipulate
+ the item disarmed (they raise NotImplementedError). The ones that are left
+ fire appropriate events on the qube that owns an instance of this class.
+ '''
+
+ #
+ # Those are the methods that affect contents. Either disarm them or make
+ # them report appropriate events. Good approach is to rewrite them carefully
+ # using official documentation, but use only our (overloaded) methods.
+ #
+ def __init__(self, vm, seq=()):
+ super(Tags, self).__init__()
+ self.vm = vm
+ self.update(seq)
+
+ def clear(self):
+ '''Remove all tags'''
+ for item in tuple(self):
+ self.remove(item)
+
+ def symmetric_difference_update(self, *args, **kwargs):
+ '''Not implemented
+ :raises: NotImplementedError
+ '''
+ raise NotImplementedError()
+
+ def intersection_update(self, *args, **kwargs):
+ '''Not implemented
+ :raises: NotImplementedError
+ '''
+ raise NotImplementedError()
+
+ def pop(self):
+ '''Not implemented
+ :raises: NotImplementedError
+ '''
+ raise NotImplementedError()
+
+ def discard(self, elem):
+ '''Remove a tag if present'''
+ if elem in self:
+ self.remove(elem)
+
+ def update(self, *others):
+ '''Add tags from iterable(s)'''
+ for other in others:
+ for elem in other:
+ self.add(elem)
+
+ def add(self, elem):
+ '''Add a tag'''
+ allowed_chars = string.ascii_letters + string.digits + '_-'
+ if any(i not in allowed_chars for i in elem):
+ raise ValueError('Invalid character in tag')
+ if elem in self:
+ return
+ self.vm.fire_event('domain-tag-add', tag=elem)
+ super(Tags, self).add(elem)
+
+ def remove(self, elem):
+ '''Remove a tag'''
+ super(Tags, self).remove(elem)
+ self.vm.fire_event('domain-tag-delete', tag=elem)
+
+ #
+ # end of overriding
+ #
+
+
class BaseVM(qubes.PropertyHolder):
'''Base class for all VMs
@@ -192,7 +269,7 @@ class BaseVM(qubes.PropertyHolder):
self.devices = devices or qubes.devices.DeviceManager(self)
#: user-specified tags
- self.tags = tags or {}
+ self.tags = Tags(self, tags or ())
#: logger instance for logging messages related to this VM
self.log = None
@@ -223,7 +300,7 @@ class BaseVM(qubes.PropertyHolder):
# tags
for node in self.xml.xpath('./tags/tag'):
- self.tags[node.get('name')] = node.text
+ self.tags.add(node.get('name'))
# SEE:1815 firewall, policy.
@@ -262,7 +339,6 @@ class BaseVM(qubes.PropertyHolder):
tags = lxml.etree.Element('tags')
for tag in self.tags:
node = lxml.etree.Element('tag', name=tag)
- node.text = self.tags[tag]
tags.append(node)
element.append(tags)
diff --git a/qubes/vm/qubesvm.py b/qubes/vm/qubesvm.py
index 9e3477c5..8ef93d8c 100644
--- a/qubes/vm/qubesvm.py
+++ b/qubes/vm/qubesvm.py
@@ -304,6 +304,22 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
:param event: Event name (``'domain-feature-set'``)
:param key: feature name
+ .. event:: domain-tag-add (subject, event, tag)
+
+ A tag was added.
+
+ :param subject: Event emitter (the qube object)
+ :param event: Event name (``'domain-tag-add'``)
+ :param tag: tag name
+
+ .. event:: domain-tag-delete (subject, event, tag)
+
+ A feature was removed.
+
+ :param subject: Event emitter (the qube object)
+ :param event: Event name (``'domain-tag-delete'``)
+ :param tag: tag name
+
.. event:: feature-request (subject, event, *, untrusted_features)
The domain is performing a feature request.