Kategorien
IT SharePoint

SharePoint – Tenant-OU wieder freigeben

SharePoint 2016 hat (anders als SharePoint 2019) noch Multi-Tenancy-Unterstützung. Das heisst, man kann einige Service-Applikationen, wie z.B. den Managed Metadata Service oder den User Profile Service für verschiedene Tenants Partitionieren – im Prinzip das, was Microsoft auch in SharePoint Online macht, um verschiedenen Kunden einen eigenen Bereich in den Service-Applikationen zu geben.

Beim erstellen eines Tenants, wird mit New-SPSiteSubscription so eine Partition für den Tenant registriert.

Mann kann dann mit dieser Subscription z.B. den People Picker auf eine OU (Organizational Unit im Active Directory) einschränken:

Set-SPSiteSubscriptionConfig -identity $siteSubscription -FeaturePack $defaultfeaturePackId -UserAccountDirectoryPath "OU=UserAccounts,DC=FABRIKAM,DC=COM"

Beim Erstellen einer Site Collection für den Tenant wird diese Subscription auch mitgegeben:

New-SPSite -Url https://fabrikam.sptest.local" -SiteSubscription $siteSubscription -HostHeaderWebApplication "https://root.sptest.local"

Und mit folgendem Befehl Add-SPSiteSubscriptionProfileConfig wird eingestellt, dass für diese Subscription die AD-Benutzer mit der entsprechenden OU synchronisiert werden (für die User Profile Service Application).

$upaProxy = Get-SPServiceApplicationProxy | where-object { $_.DisplayName -eq "MT User Profile Service Application" }
Add-SPSiteSubscriptionProfileConfig -Identity $siteSubscription -SynchronizationOU "OU=UserAccounts,DC=FABRIKAM,DC=COM" -ProfileServiceApplicationProxy $upaProxy

Wenn man zu einem späteren Zeitpunkt den Tenant löschen möchte, zum Beispiel weil beim Erstellungsprozess etwas schief gelaufen ist, dann muss man schlau vorgehen, damit die OU wiederverwendet werden kann. Eine OU kann nämlich nicht von mehr als einer Subscription verwendet werden.

Mit folgenden Schritten (in der SharePoint Management Shell) kann man eine Subscription und die zugehörige Subscription-Profile-Config löschen. Achtung: es werden dabei alle Site Collections gelöscht, welche zur jeweiligen Subscription gehören!

Zuerst holt man sich die jeweilige Subscription, welche gelöscht werden soll, zum Beispiel über den URL der Site:

# Site holen
$site = Get-SPSite "http://fabrikam.sptest.local"

# subscription der Site holen
$subscription = $site.SiteSubscription

Dann muss man sich die User Profile Service Application holen (darauf achten, dass die richtige Multi-Tenancy-fähige Service-Applikation geholt wird, falls ihr mehere, z.B. auch eine normale, in der Farm habt).

# Namen des UPA Proxys angeben
$upaProxyName = "MT User Profile Service Application"

# UPA Proxy holen
$upaProxy = Get-SPServiceApplicationProxy | where-object { $_.DisplayName -eq $upaProxyName }

Nun kann man als erstes die OU-Synchronisierung entfernen, es darf auf keinen Fall zuerst die Subscription direkt gelöscht werden, denn das wäre möglich und würde das ausführen dieses Befehls verunmöglichen, was dazu führt, dass man die OU nur noch über die manuelle Bearbeitung der Datenbank erledigen kann (was Microsoft nicht gerne hat):

# UPA Subscription Eintrag entfernen (aus UPA Profile DB für Profile Sync.)
Remove-SPSiteSubscriptionProfileConfig -Identity $subscription -ProfileServiceApplicationProxy $upaProxy

Nun kann die Subscription gelöscht werden. Um Sicherzugehen, welche Sites betroffen sind, kann man zuerst $subscription.Sites aufrufen:

# Site subscription und alle assozierten Sites löschen
Remove-SPSiteSubscription -Identity $subscription

Am Ende kann noch die Inhaltsdatenbank gelöscht werden, in welcher die Site Collection für den Tenant gespeichert war (sinnvoll falls man pro Tenant eine eigene Inhaltsdatenbank verwendet):

# Zum Schluss die Inhaltsdatenbank entfernen. Auch hier sollte man zuerst prüfen, dass keine anderen Sites in der Inhaltsdatenbank sind! (Befehl geht auch, wennd die Site zuvor gelöscht wurde, das Objekt hat noch den Wert der Datenbank und den Datenbanknamen)
$databaseName = $site.ContentDatabase.Name
Remove-SPContentDatabase -Identity $databaseName

Dies war der Weg um einen Tenant sauber zu löschen, so dass die OU die verwendet wurde, auch später wieder neu verwendet werden kann.

Falls man diesen Weg nicht eingehalten hat und z.B. die Site-Subscription schon gelöscht hat, ohne zuerst Remove-SPSiteSubscriptionProfileConfig auszuführen, dann bleibt einem nur noch der Weg, die Subscription manuell aus der Datenbank zu entfernen, dies führt gemäss Microsoft zum Verlust der Garantie!

Es handelt sich dabei um die Profile DB der User Profile Service Application, in welcher in diversen Tabellen die Einträge für unsere Subscription (Partition-ID) gelöscht werden müssen.

Nachfolgend ein SQL-Script, mit welchem man dies machen könnte, Verwendung aber auf eigene Gefahr. Wichtig ist, dass ihr in der ersten Zeile den Wert der Variable SubscriptionId auf die entsprechende Subscription-ID setzt und danach natürlich [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa] mit der ensprechenden Datenbank und dem entsprechenden Schema für euren Fall:

DECLARE @SubscriptionId AS Varchar(255) = '6d947c20-f876-4d4b-b680-1f0cd0a78c6f'

Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].ProfileSubtypeList WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].PersonalSite WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].Orgle_Stats WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].Orgle_OpList WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].Orgle_List WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].Orgle_ErrorMsg WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].InternalDetail WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].DataTypeList WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].DataServiceList WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].BucketInfo WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].MicrofeedPostDefinitions WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].ActivityTemplate WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].ActivityApplication WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].ActivityType WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].ActivityApplication WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].Profile_SearchStopwords WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].Profile_SearchUserTable WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].OrganizationProfileValue WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].OrganizationProfile WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].UserProfileEventLog WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].UserProfileValue WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].UserProfile_Full WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].UserPrivacyPolicy WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].ADImportConstantPropertyMapping WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].ADImportPropertyMapping WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].ProfileSubtypePropertyAttributes WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].ProfileTypePropertyAttributes WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].PropertyList WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].[Profile_Stats] WHERE PartitionID = @SubscriptionId
Delete
  FROM [SharePoint_MT_User_Profile_Service_Application_ProfileDB].[upa].Tenants WHERE PartitionID = @SubscriptionId

Es empfiehlt sich danach etwas zu warten, da diese Änderungen nicht sofort ziehen. Im Idealfall könnt ihr danach einen IISReset auf allen SharePoint-Servern durchführen und ich würde auch die Instanz auf dem Datenbankserver neu starten.

Kategorien
IT Python

QooliImapEmailExtractor – Ein IMAP Email-Adressen-Extrahierer in Python

Ich wurde kürzlich angefragt, ob ich nicht ein Programm kennen würde, um aus einen Mail-Postfach alle Email-Adressen zu extrahieren. Egal, ob diese im „To“- oder „From“- oder „CC“-Feld stehen.

Leider kannte ich kein Programm, dass dafür geeignet ist, obwohl es ein solches mit Sicherheit aber gibt. Aber bei vielen Gratis-Tools oder auch Trials kann man sich nicht sicher sein, ob es Malware enthält oder ob am Ende die Email-Adressen die extrahiert wurden auch noch gleich an den Hersteller des Programms gesendet werden.

Warum diese Aufgabe also nicht einfach selber mit Python lösen? Im Internet wurde ich schnell fündig und fand die Basis für ein Skript, welches schon einmal grundsätzlich alle Adressen aus einem einzelnen IMAP-Inbox-Folder eines Postfachs (mittels imaplib) in eine Variable extrahiert: https://www.quora.com/Is-there-a-way-to-extract-email-addresses-from-all-emails-in-my-Gmail-account/answer/Jerry-Neumann

Das Skript habe ich dann etwas schöner formatiert und erweitert, dass alle Folder eines Postfachs durchlaufen und die Daten dann in eine CSV-Datei exportiert werden. Das sieht dann wie folgt aus:

import imaplib
import email
import csv
import re


list_response_pattern = re.compile(r'\((?P<flags>.*?)\) "(?P<delimiter>.*)" (?P<name>.*)')


def parse_list_response(line):
    flags, delimiter, mailbox_name = list_response_pattern.match(line).groups()
    mailbox_name = mailbox_name.strip('"')
    return flags, delimiter, mailbox_name


def split_addresses(s):
    # split an address list into list of tuples of (name,address)
    if not s:
        return []
    out_q = True
    cut = -1
    res = []
    for i in range(len(s)):
        if s[i] == '"':
            out_q = not out_q
        if out_q and s[i] == ',':
            res.append(email.utils.parseaddr(s[cut + 1:i]))
            cut = i
    res.append(email.utils.parseaddr(s[cut + 1:i + 1]))
    return res


def main():
    print 'Processing started...'
    addresses = []
    mail = imaplib.IMAP4_SSL('imap.server.com')  # enter hostname eg: imap.gmail.com
    mail.login('user@server.com', 'Password')  # userid, password
    response_code_folders, mail_folder_lines = mail.list()
    for mail_folder_line in mail_folder_lines:
        flags, delimiter, mailbox_name = parse_list_response(mail_folder_line)
        print '  Extracting from folder: ' + mailbox_name
        mail.select(mailbox_name)
        result, data = mail.search(None, "ALL")
        ids = data[0].split()
        mail_count = len(ids)
        print '   Mails found in folder: ' + str(mail_count)
        if mail_count > 0:
            msgs = mail.fetch(','.join(ids), '(BODY.PEEK[HEADER])')[1][0::2]
            mail_ix = 0
            for x, msg in msgs:
                mail_ix += 1
                msgobj = email.message_from_string(msg)
                print '   extracting data from message ' + str(mail_ix) + '/' + str(len(msgs))
                addresses.extend(split_addresses(msgobj['to']))
                msgobj = email.message_from_string(msg)
                addresses.extend(split_addresses(msgobj['from']))
                addresses.extend(split_addresses(msgobj['cc']))
    # only take unique addresses
    addresses = set(addresses)
    csv_filename = 'email_addresses_export.csv'
    with open(csv_filename, 'wb') as out:
        csv_out = csv.writer(out)
        csv_out.writerow(['Name', 'Address'])
        for row in addresses:
            csv_out.writerow(row)
    print ' CSV with addresses written to: ' + csv_filename
    print 'Processing finished'


main()

Wenn ihr das Skript verwenden wollt, müsst ihr Python installiert haben und euren Mailserver und die Postfach-Logindaten eintragen:

mail = imaplib.IMAP4_SSL(‚imap.server.com‚)  # IMAP-Hostname
mail.login(‚user@server.com‚, ‚Password‚)  # Postfach-Benutzer-ID, Passwort

Kategorien
IT SQL

Microsoft SQL Server Management Studio – verbleibende Datenbank-Restore-Dauer anzeigen

Vielleicht habt ihr auch schon mal in einem Microsoft SQL Server einen Restore einer Datenbank durchgeführt und wolltet wissen, wie weit der Restore nach einigen Minuten/Stunden schon gekommen ist und wann der Restore ungefähr beendet sein wird.

Wird der Restore via „Management Studio“-GUI durchgeührt (Datenbank-Kontextmenü -> Tasks -> Restore), dann habt ihr im Restore-Dialog zumindest eine ungefähre Prozent-Anzeige. In der 2012er-Version wird diese oben rechts angezeigt:

Die Prozent-Anzeige erscheint aber erst nach einem Weilchen und wenn man den Restore via SQL-Skript gestartet hat und dabei „STATS“ nicht (oder mit einem ungünstigen Wert) verwendet hat, weiss man auch nicht bei wieviel Prozent der Restore aktuell steht und wie lange er noch dauert. Häufig bleibt die Prozentzahl auch lange unverändert und man ist sich nicht sicher, ob der Restore noch läuft oder sich aufgehängt hat.

Zum Glück gibt es jedoch eine Möglichkeit mit einem SQL-Befehl den aktuellen Fortschritt und die geschätzte Endzeit zu erhalten – egal ob  der Backup-Job via GUI, Skript oder sogar von einer anderen Maschine gestartet wurde. Voraussetzung ist ein MSSQL Server der Dynamic Management Views (DMVs) unterstützt, also ab MSSQL 2005. Ist das gegeben kann man folgenden Befehl als Query absetzen (es muss keine Datenbank dazu vorselektiert werden):

SELECT session_id as SPID, command, a.text AS Query, start_time, percent_complete, dateadd(second,estimated_completion_time/1000, getdate()) as estimated_completion_time
FROM sys.dm_exec_requests r CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) a
WHERE r.command in ('BACKUP DATABASE','RESTORE DATABASE')

Das Resultat sieht dann zum Beispiel wie folgt aus (unterer Bereich, Spalten percent_complete und estimated_completion_time):

Im Beispiel sieht man also, dass bereits 96.16975 % des Restores durchlaufen ist und der Restore um 12:38 Uhr, des 7. Dezember 2016, fertig sein sollte – nach ungefähr 19 Stunden Laufzeit. Praktisch oder?

Gefunden auf: https://www.mssqltips.com/sqlservertip/2343/how-to-monitor-backup-and-restore-progress-in-sql-server/

Kategorien
IT

MySQL- und Website-Backup periodisch erstellen

Vor einigen Tagen konnte ich einen Punkt von meiner Aufgabenliste streichen, welchen ich schon längere Zeit offen hatte und eigentlich schon lange einmal hätte umsetzen wollen und sollen: das Einrichten eines Backups für meine Webseite.

Bisher machte ich nur sporadisch entweder manuelle Backups der Daten der Webverzeichnisse und der dazugehörigen MySQL-Datenbanken oder ich habe bei meinem VPS-Provider ein Backup für die ganze Maschine durchgeführt – beides nicht gerade sehr sexy, das muss besser und eleganter gelöst werden, dachte ich mir.

Als erstes schrieb ich ein Shell-Skript, welches mir alle Datenbanken (ausser den System-Datenbanken) in Archiv-Dateien exportiert (alle Pfade und Benutzerangaben wurden durch Beispieldaten ersetzt):

#!/bin/bash

BACKUP_DATE="$(date +%Y-%m-%d)"
DB_BACKUP_ROOT="/zielpfad/zum/backup/mysql"
DB_BACKUP="$DB_BACKUP_ROOT$BACKUP_DATE"
DB_USER="Datenbank-Benutzer"
DB_PASSWD="Datenbank-Kennwort"
HN=`hostname | awk -F. '{print $1}'`
# Create the backup directory
mkdir -p $DB_BACKUP

# Remove backups older than 30 days
find $DB_BACKUP_ROOT -maxdepth 1 -type d -mtime +30 -exec rm -rf {} \;

# Backup each database on the system using username and password
for db in $(mysql --user=$DB_USER --password=$DB_PASSWD -e 'show databases' -s --skip-column-names|grep -vi information_schema);
do
  BACKUP_FILE_PATH="$DB_BACKUP/mysqldump-$HN-$db-$BACKUP_DATE.gz"
  mysqldump --user=$DB_USER --password=$DB_PASSWD --opt $db | gzip > $BACKUP_FILE_PATH;
  # Set desired permissions
  chmod -R 600 $BACKUP_FILE_PATH
done

Das obenstehende Skript macht folgendes:

  • Zuerst setze ich einige Variablen, wie das Zielverzeichnis für die Backup-Dateien. Ich lese das aktuelle Datum aus und speichere es in einer Variable und benutze dies, um für die täglichen Backups jeweils ein neues Zielverzeichnis innerhalb des festgelegten „DB_BACKUP_ROOT“ zu erstellen.
  • Danach setze ich die MySQL-Zugangsdaten und ermittle den Host-Namen des Rechners, um diesen später auch zum Dateipfad hinzuzufügen.
  • Mittels des „find“-Befehls ermittle ich alle Backups, welche älter als 30 Tage sind und lösche diese.
  • Danach iteriere ich in einer Schleife über alle relevanten Datenbanken und speichere diese mittels „mysqldump“ an den gewünschten Zielort, dabei pipe ich das Resultat von „mysqldump“ zuvor noch an „gzip“ und sorge damit, dass ich eine Archivdatei erhalte.
  • Zum Schluss ändere ich mittels „chmod -R 600 …“ noch die Dateirechte der zuvor erstellten Backup-Datei, so dass nur noch der Besitzer der Datei diese lesen und ändern kann.

Nun habe ich die Datenbank gesichert. Mittels eines weiteren Shells-Skripts sichere ich nun noch die Dateien der einzelnen Webseiten:

#!/bin/bash

WWW_ROOT="/var/wwwdir/websites/"
WWW_BACKUP_ROOT="/zielpfad/zum/backup/www"
WWW_BACKUP="$WWW_BACKUP_ROOT`date +%Y-%m-%d`"
HN=`hostname | awk -F. '{print $1}'`
# Create the backup directory
mkdir -p $WWW_BACKUP

# Remove backups older than 30 days
find $WWW_BACKUP_ROOT -maxdepth 1 -type d -mtime +30 -exec rm -rf {} \;

# get arguments
while [ "$1" != "" ]; do
  case $1 in
    -s | --site )     shift
                      site=$1
                      ;;
    * )               echo "Unknown argument $1."
                      exit 1
  esac
  shift
done

# Backup website
if [ "$site" != "" ]; then
  BACKUP_PATH="$WWW_BACKUP/www-$HN-$site-$(date +%Y-%m-%d).tar.gz"
  WWW_PATH="$WWW_ROOT$site"
  echo "Backing up site $WWW_PATH to $BACKUP_PATH" 
  cd $WWW_ROOT
  tar -czf $BACKUP_PATH $site
  chmod -R 600 $BACKUP_PATH
else
  echo "You need to specifiy a site to backup with the -s argument."
  exit 1
fi

Dieses Skript macht folgendes:

  • Als erstes setze ich wieder einige Variablen. Das Quellverzeichnis der Webseiten-Daten wird angegeben (WWW_ROOT) und das (Wurzel-)Zielverzeichnis für die Backup-Daten wird festgelegt (WWW_BACKUP_ROOT). Ausserdem lege ich den Ordernnamen für die täglichen Backups fest (WWW_BACKUP), dazu verwende ich wie schon beim MySQL-Backup das „date“-Befehl.
  • Mittels mkdir -p $WWW_BACKUP wird der tägliche Backup-Ziel-Ordner erstellt, es sei denn er ist schon vorhanden (dazu ist der „-p“-Parameter da).
  • Erneut suche ich mittels „find“ alle Backup-Dateien, welche älter sind als 30 Tage und lösche diese.
  • Danach kommt eine „while“-Schleife, mit der ich die Argumente, welche dem Skript übergeben wurden auslese, dies ist nötig, da ich das Skript später mittels Cron-Job aufrufen möchte und jeweils die Quell-Webseite mittels eines Parameters angeben möchte. Wenn keine Website als Argument übergeben oder falls ein unbekanntes Argument übergeben wurde, wird das Skript beendet (exit 1).
  • Danach wird die eigentliche Backup-Datei erstellt. Dazu wird der Zielpfad der Archivdatei zusammengesetzt (BACKUP_PATH) und dann mittels dem „tar“-Befehl das Backup erstellt und gepackt.
  • Am Ende passe ich wieder die Dateirechte mittels „chmod“ an, so dass nur der Besitzer die Datei lesen und ändern kann.

Nun haben wir zwei Skripts, welche die für mich relevanten Komponenten der Webseiten sichern können, jedoch müssten diese immernoch manuell ausgeführt werden. Damit automatisch täglich die Backups erstellt werden, richtete ich unter einem Account einige Cronjobs (Befehl: crontab -e) ein, welche die (ausführbaren) Skripte einmal am Tag laufen lassen:

# Daily website backup
15 2 * * * /usr/local/bin/mysql_backup.sh >/dev/null 2>&1
15 2 * * * /usr/local/bin/www_backup.sh -s website_dir_1 >/dev/null 2>&1
15 2 * * * /usr/local/bin/www_backup.sh -s website_dir_2 >/dev/null 2>&1
15 2 * * * /usr/local/bin/www_backup.sh -s website_dir_3 >/dev/null 2>&1
45 4 * * * /usr/local/bin/copy_backup_to_different_server.sh >/dev/null 2>&1

Die Backups werden mit dieser Konfiguration also immer um 2:15 Uhr durchgeführt. „/dev/null 2>&1“ bewirkt, dass die Ausgaben unterdrückt werden, sie landen im Nirvana.

In der letzten Zeile der obenstehenden Crontab seht ihr, dass noch ein weiteres Skript „copy_backup_to_different_server.sh“ zu einem späteren Zeitpunkt aufgerufen wird.
Da die Daten bisher nur lokal auf dem Server gesichert werden, nützen sie mir nichts, falls es zu einem generellen Datenverlust auf dem Server käme. Zur Sicherheit kopiere ich also die Backup-Archive noch an einen weiteren Ort, auf einen anderen Server.

Das  Skript ist wie folgt aufgebaut:

  • #!/bin/bash
    
    rsync -rlthvz --delete --owner=1111 --group=333 -e '/usr/bin/ssh -i /home/userXy/.ssh/id_rsa' /zielpfad/zum/backup/ user@remote.server.com:/volumeXy/home/userMn/MyFiles/Backup/websites<span id="mce_marker" data-mce-type="bookmark" data-mce-fragment="1">​</span>

    Das Skript kopiert via RSYNC alle neuen Dateien von „/zielpfad/zum/backup“ auf den Server „remote.server.com“ in den Pfad „/volumeXy/home/userMn/MyFiles/Backup/websites“. Die Verbindung wird im Beispiel mit dem Benutzer „user“ durchgeführt.

  • Die Verbindung geschieht durch den SSH-Aufruf innerhalb des RSYNC-Befehls über eine verschlüsselte SSH-Verbindung.
  • „–delete“ bewirkt, dass im Ziel die Dateien gelöscht werden, welche nicht mehr in der Quelle vorhanden sind.
  • Mittels „–owner=…“ und „–group=…“ setze ich den Besitzer und die Gruppe der neuen Dateien und Ordner auf dem Zielsystem, da diese nicht mit denen auf dem Quellsystem identisch sind.

Damit RSYNC via SSH auch ohne Kennwort-Eingabe funktioniert, muss man natürlich zuerst ein Schlüssel-Paar (öffentlich/privat) erzeugen (sofern man noch keines hat) und den öffentlichen Schlüssel auf dem Zielsystem in den „Authorized Keys“ eintragen. Hier findet ihr eine Anleitung dazu: http://www.thegeekstuff.com/2008/11/3-steps-to-perform-ssh-login-without-password-using-ssh-keygen-ssh-copy-id/

Nun ist das Backup also eingerichtet. Es sichert täglich automatisch die Websites mitsamt der Datenbank und kopiert diese Dateien zur Sicherheit auch noch auf einen anderen Server.

Kategorien
IT

Feeds parsen und schreiben mit Python

Ich hatte letzthin das Problem, dass ich auf meiner neuen Fotografie-Seite einen Feed meiner letzten iStockphoto-Uploads einbinden wollte, aber dies nicht ging, da der offizielle Feed scheinbar nicht validiert:
http://validator.w3.org/feed/check.cgi?url=http%3A%2F%2Fwww.istockphoto.com%2Fistock_myfiles_rss.php%3FID%3D8197370

Auf WordPress verwendete ich zuerst das „Embed RSS„-Plugin um Feeds in die Seite zu integrieren, jedoch kam dies nicht mit diesem ungültigen Feed klar. Einige RSS-Reader-Programme (z.B. „Vienna RSS„) hatten jedoch keine Probleme mit dem Feed. Vermutlich korrigierten sie intern die Fehler oder ignorierten diese einfach.

Zum Glück hat mich einmal ein guter Freund in die Welt von Python eingeführt und mir die nötigen Basics dieser tollen Programmiersprache beigebracht.

Mit Hilfe der Bibliotheken „feedparser“ und „PyRSS2Gen“ konnte ich ein Python-Skript schreiben um dieses Problem umgehen.

Zuerst hole ich mit feedparser den Feed ab und baue später mit PyRSS2Gen einen neuen Feed im „RSS 2.0“-Format neu auf. Abschliessend schreibe ich diesen neuen Feed als XML-Datei in das Verzeichnis des Webservers.

Das Skript habe ich so umgebaut, dass eine beliebige iStockphoto-Benutzer-ID als Parameter übergeben werden kann. Auch die Ziel-Datei kann als Paramter übergeben werden.

Hier das Skript, welches in einem Repository auf Github gehosted ist:

#!/usr/bin/python

import datetime
import feedparser
import getopt
import os
import PyRSS2Gen
import sys

def create_latest_uploads_feed(targetPath, feedTitle, feedUrl, feedDescription, entries):
  rssItems = []
  for entry in entries:
    rssItem = PyRSS2Gen.RSSItem(
        title = entry['title'],
        link = entry['link'],
        description = entry['description'],
        guid = PyRSS2Gen.Guid(entry['link']),
        pubDate = entry['pubDate'],
        author = entry['author']
        )
    rssItems.append(rssItem)
  rss = PyRSS2Gen.RSS2(
      title = feedTitle,
      link = feedUrl,
      description = feedDescription,
      lastBuildDate = datetime.datetime.now(),
      items = rssItems)
  print 'Writing RSS feed: ' + targetPath
  rss.write_xml(open(targetPath, 'w'))
  print 'Done!'


def fetch_feed_and_create_rss(userId, targetPath):
  feedUrl = "http://www.istockphoto.com/istock_myfiles_rss.php?ID=" + userId
  print 'Fetching feed: ' + feedUrl
  feed = feedparser.parse(feedUrl)
  feedTitle = feed['feed']['title']
  feedDescription = ""
  entries = feed['entries']
  preparedEntries = []
  print 'Processing fetched entries. Entries found: ' + str(len(entries))
  for entry in entries:
    entryPubDate = entry['published']
    pubDate = datetime.datetime.strptime(entryPubDate[:-6], '%Y-%m-%dT%H:%M:%S')
    #pubDateClean = pubDate.strftime('%d.%m.%Y')
    author = entry['author']
    entryTitle = entry['title']
    print ' * Entry title: ' + entryTitle.encode('utf-8')
    link = entry['link']
    summaryHtml = entry['summary']
    preparedEntries.append({"pubDate":pubDate,"author":author,"title":entryTitle,
      "description":summaryHtml,"link":link})
  create_latest_uploads_feed(targetPath, feedTitle, feedUrl, feedDescription,
      preparedEntries)

def main(argv):
  scriptName = os.path.basename(__file__)
  usageInfo = 'usage: ' + scriptName + ' -i  -t '
  userId = ''
  targetPath = ''
  try:
    opts, args = getopt.getopt(argv,"hi:t:",["id=","targetpath="])
  except getopt.GetoptError:
    print usageInfo
    sys.exit(2)
  for opt, arg in opts:
    if opt == '-h':
      print usageInfo
      sys.exit()
    elif opt in ("-i", "--id"):
      userId = arg
    elif opt in ("-t", "--targetpath"):
      targetPath = arg
  if (userId and targetPath):
    print 'User ID is: ' + userId
    print 'Target path is: ' + targetPath
    fetch_feed_and_create_rss(userId, targetPath)
  else:
    print usageInfo
    sys.exit()


if __name__ == "__main__":
  main(sys.argv[1:])
istock_latest_uploads.pyview rawview file on GitHub

Genau genommen validiert der neue Feed immer noch nicht 🙂 allerdings sind die Fehler nun scheinbar weniger tragisch und er kann nun von „Embed RSS“ gelesen werden. Auch verwende ich nun zur Einbettung in WordPress „HungryFEED“ anstelle von „Embed RSS“, da dieses etwas mächtiger ist und bessere Templating-Option mitbringt.

Damit ich für die Webseite immer einen relativ aktuellen Feed erhalte, lasse ich das Skript mittels Crontab alle vier Stunden laufen:

#m h dom mon dow command
0 */4 * * * python /usr/local/bin/scriptXy.py -i 8197370 -t /dir/subdir/targetfile.xml
Kategorien
IT SharePoint

SharePoint: Inhaltsdatenbank-Version an SharePoint-Version angleichen

Wenn man in einer SharePoint-Installation ein neues Cumulatives Update installiert um Programm-Fehler zu beheben, so wird die gesamte SharePoint-Version hochgehoben, also die Assemblies/Dlls etc. aktualisiert und haben dadurch höhere Versionsnummern.

Die Inhaltsdatenbank (Content Database) wird grundsätzlich aber nicht automatisch aktualisiert und behält das alte Schema und auch die alte Versionsnummer bei.

In der Regel ist dies nicht weiter tragisch, aber unter Umständen kann es trotzdem nötig werden, dass man die Inhaltsdatenbank aktualisiert und damit ihre Version an diejenige des SharePoint-Serves angleicht, so zum Beispiel, wenn man ein Backup einer Site-Collection einspielen möchte (Restore-SPSite), welches von einer Site/Datenbank mit der höheren Versionsnummer, die des Servers, entstammt.

Um manuell die Inhaltsdatenbank zu aktualisieren, kann man sich der SharePoint Management Shell bedienen.

Zuerst sollte man die gewünschte Content Database in eine Variable laden (wobei man „SQLServerName\WSS_Content_Database_Name“ durch den entsprechenden Datenbank-Server und Namen der Inhaltsdatenbank ersetzen muss):

$contentdb = Get-SPContentDatabase | Where-Object {$_.Name -match "SQLServerName\WSS_Content_Database_Name"}

Ist unklar, wie die Inhaltsdatenbank heisst, so kann man zuerst einfach nur Get-SPContentDatabase eingeben um eine Auflistung aller Inhaltsdatenbanken zu erhalten. Sollte die gewünschte Datenbank nicht aufgeführt sein, sollte sicher gestellt werden, dass diese in der Central Administration im Status „Ready“ ist.

Danach kann man mit folgendem PowerShell-Befehl die Aktualisierung starten:

Upgrade-SPContentDatabase -Identity $contentdb

In der PowerShell wird der Fortschritt in Prozent angezeigt. Bei grösseren Datenbanken kann die Aktualsierung mehrere Minute bis hin zu Stunden dauern.

Weitere Informationen findet ihr in diesem Technet-Artikel: http://technet.microsoft.com/en-us/library/ff607813.aspx

Kategorien
IT SharePoint

SharePoint: TaxonomyHiddenList manuell aktualisieren

Unter gewissen Umständen und mit gewissen Cumulativen Updates für den SharePoint 2010 kann es vorkommen, dass die TaxonomyHiddenList nicht mehr auf dem aktuellsten Stand ist, da die Synchronisation mit dem Term-Store (Terminologiespeicher) nicht mehr fehlerfrei durchgeführt werden kann.

Abhilfe (wenn auch eventuell nur temporäre?)  schafft folgendes PowerShell-Skript:

$Assem = (
	"Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" ,
	"Microsoft.SharePoint.Taxonomy, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"
)

$Source = @"
using Microsoft.SharePoint;
using Microsoft.SharePoint.Taxonomy;
using System;

namespace TaxonomyUpdater
{
	public static class TaxonomyHiddenList
	{
		public static void Update(string SiteUrl)
		{
			SPSite Site2Update = new SPSite(SiteUrl);
			TaxonomySession.SyncHiddenList(Site2Update);
			Site2Update.Dispose();
		}
	}
}
"@

Add-Type -ReferencedAssemblies $Assem -TypeDefinition $Source -Language CSharp

Das Skript muss man in einen Text-Editor einfügen und als „UpdateTaxonomyHiddenList.ps1“ abspeichern.

Danach das Skript in einer PowerShell einmal ausführen, damit die Klasse verfügbar wird. Zum Schluss mit folgendem Befehl die Synchronisation starten:

[TaxonomyUpdater.TaxonomyHiddenList]::Update(“http://SharePointServer”)

Siehe auch: http://blogs.msdn.com/b/joerg_sinemus/archive/2011/03/03/terms-and-how-to-update-taxonomyhiddenlist-when-the-timer-job-was-not-able-to-update.aspx